Merge branch 'develop' into freqai_feature_engineering_functions
This commit is contained in:
commit
ed2b1b1ed1
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@ -147,15 +147,20 @@ jobs:
|
|||||||
- name: Installation - macOS
|
- name: Installation - macOS
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
# homebrew fails to update python 3.9.1 to 3.9.1.1 due to unlinking failure
|
brew update
|
||||||
|
# homebrew fails to update python due to unlinking failures
|
||||||
|
# https://github.com/actions/runner-images/issues/6817
|
||||||
rm /usr/local/bin/2to3 || true
|
rm /usr/local/bin/2to3 || true
|
||||||
# homebrew fails to update python from 3.9 to 3.10 due to another unlinking failure
|
rm /usr/local/bin/2to3-3.11 || true
|
||||||
rm /usr/local/bin/idle3 || true
|
rm /usr/local/bin/idle3 || true
|
||||||
|
rm /usr/local/bin/idle3.11 || true
|
||||||
rm /usr/local/bin/pydoc3 || true
|
rm /usr/local/bin/pydoc3 || true
|
||||||
|
rm /usr/local/bin/pydoc3.11 || true
|
||||||
rm /usr/local/bin/python3 || true
|
rm /usr/local/bin/python3 || true
|
||||||
|
rm /usr/local/bin/python3.11 || true
|
||||||
rm /usr/local/bin/python3-config || true
|
rm /usr/local/bin/python3-config || true
|
||||||
# Ignore brew update failures - https://github.com/actions/runner-images/issues/6817
|
rm /usr/local/bin/python3.11-config || true
|
||||||
brew update || true
|
|
||||||
brew install hdf5 c-blosc
|
brew install hdf5 c-blosc
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
|
@ -15,7 +15,7 @@ repos:
|
|||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.2.1
|
- types-cachetools==5.2.1
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.11.5
|
- types-requests==2.28.11.7
|
||||||
- types-tabulate==0.9.0.0
|
- types-tabulate==0.9.0.0
|
||||||
- types-python-dateutil==2.8.19.5
|
- types-python-dateutil==2.8.19.5
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
|
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
|
||||||
|
|
||||||
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
||||||
|
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
|
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
|
||||||
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
@ -300,7 +300,11 @@ A backtesting result will look like that:
|
|||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Sortino | 1.88 |
|
||||||
|
| Sharpe | 2.97 |
|
||||||
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
|
| Expectancy | -0.15 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
@ -400,7 +404,11 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Sortino | 1.88 |
|
||||||
|
| Sharpe | 2.97 |
|
||||||
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
|
| Expectancy | -0.15 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
@ -447,6 +455,9 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Absolute profit`: Profit made in stake currency.
|
- `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`.
|
- `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.
|
- `CAGR %`: Compound annual growth rate.
|
||||||
|
- `Sortino`: Annualized Sortino ratio.
|
||||||
|
- `Sharpe`: Annualized Sharpe ratio.
|
||||||
|
- `Calmar`: Annualized Calmar ratio.
|
||||||
- `Profit factor`: profit / loss.
|
- `Profit factor`: profit / loss.
|
||||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
- `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.
|
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||||
|
@ -15,7 +15,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String.
|
| `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String.
|
||||||
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: `0` (models retrain as often as possible).
|
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: `0` (models retrain as often as possible).
|
||||||
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire).
|
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire).
|
||||||
| `purge_old_models` | Delete obsolete models. <br> **Datatype:** Boolean. <br> Default: `False` (all historic models remain on disk).
|
| `purge_old_models` | Delete all unused models during live runs (not relevant to backtesting). If set to false (not default), dry/live runs will accumulate all unused models to disk. If <br> **Datatype:** Boolean. <br> Default: `True`.
|
||||||
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
||||||
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
||||||
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
@ -365,7 +365,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
timeframe = '15m'
|
timeframe = '15m'
|
||||||
minimal_roi = {
|
minimal_roi = {
|
||||||
"0": 0.10
|
"0": 0.10
|
||||||
},
|
}
|
||||||
# Define the parameter spaces
|
# Define the parameter spaces
|
||||||
buy_ema_short = IntParameter(3, 50, default=5)
|
buy_ema_short = IntParameter(3, 50, default=5)
|
||||||
buy_ema_long = IntParameter(15, 200, default=50)
|
buy_ema_long = IntParameter(15, 200, default=50)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
![freqtrade](assets/freqtrade_poweredby.svg)
|
![freqtrade](assets/freqtrade_poweredby.svg)
|
||||||
|
|
||||||
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
||||||
|
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
|
||||||
|
@ -92,6 +92,8 @@ One account is used to share collateral between markets (trading pairs). Margin
|
|||||||
"margin_mode": "cross"
|
"margin_mode": "cross"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Please read the [exchange specific notes](exchanges.md) for exchanges that support this mode and how they differ.
|
||||||
|
|
||||||
## Set leverage to use
|
## Set leverage to use
|
||||||
|
|
||||||
Different strategies and risk profiles will require different levels of leverage.
|
Different strategies and risk profiles will require different levels of leverage.
|
||||||
|
@ -11,9 +11,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}>
|
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}>
|
||||||
<div class="md-sidebar__scrollwrap">
|
<div class="md-sidebar__scrollwrap">
|
||||||
<div id="widget-wrapper">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="md-sidebar__inner">
|
<div class="md-sidebar__inner">
|
||||||
{% include "partials/nav.html" %}
|
{% include "partials/nav.html" %}
|
||||||
</div>
|
</div>
|
||||||
@ -44,25 +41,4 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
||||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<!-- Load binance SDK -->
|
|
||||||
<script async defer src="https://public.bnbstatic.com/static/js/broker-sdk/broker-sdk@1.0.0.min.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.onload = function () {
|
|
||||||
var sidebar = document.getElementById('widget-wrapper')
|
|
||||||
var newDiv = document.createElement("div");
|
|
||||||
newDiv.id = "widget";
|
|
||||||
try {
|
|
||||||
sidebar.prepend(newDiv);
|
|
||||||
|
|
||||||
window.binanceBrokerPortalSdk.initBrokerSDK('#widget', {
|
|
||||||
apiHost: 'https://www.binance.com',
|
|
||||||
brokerId: 'R4BD3S82',
|
|
||||||
slideTime: 4e4,
|
|
||||||
});
|
|
||||||
} catch(err) {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -11,18 +11,3 @@
|
|||||||
.rst-versions .rst-other-versions {
|
.rst-versions .rst-other-versions {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#widget-wrapper {
|
|
||||||
height: calc(220px * 0.5625 + 18px);
|
|
||||||
width: 220px;
|
|
||||||
margin: 0 auto 16px auto;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--md-code-bg-color);
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: calc(76.25em - 1px)) {
|
|
||||||
#widget-wrapper { display: none; }
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2022.12.dev'
|
__version__ = '2023.1.dev'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
try:
|
try:
|
||||||
|
@ -20,8 +20,8 @@ from freqtrade.persistence import LocalTrade, Trade, init_db
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Newest format
|
# Newest format
|
||||||
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount',
|
||||||
'open_rate', 'close_rate',
|
'open_date', 'close_date', 'open_rate', 'close_rate',
|
||||||
'fee_open', 'fee_close', 'trade_duration',
|
'fee_open', 'fee_close', 'trade_duration',
|
||||||
'profit_ratio', 'profit_abs', 'exit_reason',
|
'profit_ratio', 'profit_abs', 'exit_reason',
|
||||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||||
@ -241,6 +241,33 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Compatibility support for older backtest data.
|
||||||
|
"""
|
||||||
|
df['open_date'] = pd.to_datetime(df['open_date'],
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True
|
||||||
|
)
|
||||||
|
df['close_date'] = pd.to_datetime(df['close_date'],
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True
|
||||||
|
)
|
||||||
|
# Compatibility support for pre short Columns
|
||||||
|
if 'is_short' not in df.columns:
|
||||||
|
df['is_short'] = False
|
||||||
|
if 'leverage' not in df.columns:
|
||||||
|
df['leverage'] = 1.0
|
||||||
|
if 'enter_tag' not in df.columns:
|
||||||
|
df['enter_tag'] = df['buy_tag']
|
||||||
|
df = df.drop(['buy_tag'], axis=1)
|
||||||
|
if 'max_stake_amount' not in df.columns:
|
||||||
|
df['max_stake_amount'] = df['stake_amount']
|
||||||
|
if 'orders' not in df.columns:
|
||||||
|
df['orders'] = None
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load backtest data file.
|
Load backtest data file.
|
||||||
@ -269,24 +296,7 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
|||||||
data = data['strategy'][strategy]['trades']
|
data = data['strategy'][strategy]['trades']
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df['open_date'] = pd.to_datetime(df['open_date'],
|
df = _load_backtest_data_df_compatibility(df)
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
df['close_date'] = pd.to_datetime(df['close_date'],
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
# Compatibility support for pre short Columns
|
|
||||||
if 'is_short' not in df.columns:
|
|
||||||
df['is_short'] = 0
|
|
||||||
if 'leverage' not in df.columns:
|
|
||||||
df['leverage'] = 1.0
|
|
||||||
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['orders'] = None
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# old format - only with lists.
|
# old format - only with lists.
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -190,3 +192,119 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
|||||||
:return: CAGR
|
:return: CAGR
|
||||||
"""
|
"""
|
||||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_expectancy(trades: pd.DataFrame) -> float:
|
||||||
|
"""
|
||||||
|
Calculate expectancy
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||||
|
:return: expectancy
|
||||||
|
"""
|
||||||
|
if len(trades) == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
expectancy = 1
|
||||||
|
|
||||||
|
profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum()
|
||||||
|
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum())
|
||||||
|
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0])
|
||||||
|
nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0])
|
||||||
|
|
||||||
|
if (nb_win_trades > 0) and (nb_loss_trades > 0):
|
||||||
|
average_win = profit_sum / nb_win_trades
|
||||||
|
average_loss = loss_sum / nb_loss_trades
|
||||||
|
risk_reward_ratio = average_win / average_loss
|
||||||
|
winrate = nb_win_trades / len(trades)
|
||||||
|
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
|
||||||
|
elif nb_win_trades == 0:
|
||||||
|
expectancy = 0
|
||||||
|
|
||||||
|
return expectancy
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sortino
|
||||||
|
:param trades: DataFrame containing trades (requires columns profit_abs)
|
||||||
|
:return: sortino
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'] / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
|
||||||
|
down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
|
||||||
|
|
||||||
|
if down_stdev != 0:
|
||||||
|
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
|
||||||
|
sortino_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
||||||
|
return sortino_ratio
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sharpe
|
||||||
|
:param trades: DataFrame containing trades (requires column profit_abs)
|
||||||
|
:return: sharpe
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'] / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
up_stdev = np.std(total_profit)
|
||||||
|
|
||||||
|
if up_stdev != 0:
|
||||||
|
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||||
|
sharp_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||||
|
return sharp_ratio
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate calmar
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||||
|
:return: calmar
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'].sum() / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
# adding slippage of 0.1% per trade
|
||||||
|
# total_profit = total_profit - 0.0005
|
||||||
|
expected_returns_mean = total_profit / days_period * 100
|
||||||
|
|
||||||
|
# calculate max drawdown
|
||||||
|
try:
|
||||||
|
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
|
||||||
|
trades, value_col="profit_abs", starting_balance=starting_balance
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
max_drawdown = 0
|
||||||
|
|
||||||
|
if max_drawdown != 0:
|
||||||
|
calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||||
|
calmar_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||||
|
return calmar_ratio
|
||||||
|
@ -31,7 +31,7 @@ class Binance(Exchange):
|
|||||||
"ccxt_futures_name": "future"
|
"ccxt_futures_name": "future"
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2035,8 +2035,8 @@ class Exchange:
|
|||||||
# Fetch OHLCV asynchronously
|
# Fetch OHLCV asynchronously
|
||||||
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Fetching pair %s, interval %s, since %s %s...",
|
"Fetching pair %s, %s, interval %s, since %s %s...",
|
||||||
pair, timeframe, since_ms, s
|
pair, candle_type, timeframe, since_ms, s
|
||||||
)
|
)
|
||||||
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
||||||
candle_limit = self.ohlcv_candle_limit(
|
candle_limit = self.ohlcv_candle_limit(
|
||||||
@ -2050,11 +2050,12 @@ class Exchange:
|
|||||||
limit=candle_limit, params=params)
|
limit=candle_limit, params=params)
|
||||||
else:
|
else:
|
||||||
# Funding rate
|
# Funding rate
|
||||||
data = await self._api_async.fetch_funding_rate_history(
|
data = await self._fetch_funding_rate_history(
|
||||||
pair, since=since_ms,
|
pair=pair,
|
||||||
limit=candle_limit)
|
timeframe=timeframe,
|
||||||
# Convert funding rate to candle pattern
|
limit=candle_limit,
|
||||||
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
since_ms=since_ms,
|
||||||
|
)
|
||||||
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
||||||
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
|
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
|
||||||
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
|
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
|
||||||
@ -2082,6 +2083,24 @@ class Exchange:
|
|||||||
raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
|
raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
|
||||||
f'for pair {pair}. Message: {e}') from e
|
f'for pair {pair}. Message: {e}') from e
|
||||||
|
|
||||||
|
async def _fetch_funding_rate_history(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
limit: int,
|
||||||
|
since_ms: Optional[int] = None,
|
||||||
|
) -> List[List]:
|
||||||
|
"""
|
||||||
|
Fetch funding rate history - used to selectively override this by subclasses.
|
||||||
|
"""
|
||||||
|
# Funding rate
|
||||||
|
data = await self._api_async.fetch_funding_rate_history(
|
||||||
|
pair, since=since_ms,
|
||||||
|
limit=limit)
|
||||||
|
# Convert funding rate to candle pattern
|
||||||
|
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||||
|
return data
|
||||||
|
|
||||||
# Fetch historic trades
|
# Fetch historic trades
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
@ -2745,11 +2764,16 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
PERPETUAL:
|
PERPETUAL:
|
||||||
gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
|
gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
|
||||||
|
> Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
|
||||||
|
[ 1 ± (Maintenance Margin Ratio + Taker Rate)]
|
||||||
|
Wherein, "+" or "-" depends on whether the contract goes long or short:
|
||||||
|
"-" for long, and "+" for short.
|
||||||
|
|
||||||
okex: https://www.okex.com/support/hc/en-us/articles/
|
okex: https://www.okex.com/support/hc/en-us/articles/
|
||||||
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
|
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
|
||||||
|
|
||||||
:param exchange_name:
|
:param pair: Pair to calculate liquidation price for
|
||||||
:param open_rate: Entry price of position
|
:param open_rate: Entry price of position
|
||||||
:param is_short: True if the trade is a short, false otherwise
|
:param is_short: True if the trade is a short, false otherwise
|
||||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||||
@ -2789,7 +2813,7 @@ class Exchange:
|
|||||||
def get_maintenance_ratio_and_amt(
|
def get_maintenance_ratio_and_amt(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
nominal_value: float = 0.0,
|
nominal_value: float,
|
||||||
) -> Tuple[float, Optional[float]]:
|
) -> Tuple[float, Optional[float]]:
|
||||||
"""
|
"""
|
||||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
|
@ -9,8 +9,9 @@ from tabulate import tabulate
|
|||||||
|
|
||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
|
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
|
||||||
Config)
|
Config)
|
||||||
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
|
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||||
calculate_max_drawdown)
|
calculate_expectancy, calculate_market_change,
|
||||||
|
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
|
||||||
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
|
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
|
||||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
|
|
||||||
@ -448,6 +449,10 @@ def generate_strategy_stats(pairlist: List[str],
|
|||||||
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
|
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
|
||||||
'profit_total_short_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']),
|
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
|
||||||
|
'expectancy': calculate_expectancy(results),
|
||||||
|
'sortino': calculate_sortino(results, min_date, max_date, start_balance),
|
||||||
|
'sharpe': calculate_sharpe(results, min_date, max_date, start_balance),
|
||||||
|
'calmar': calculate_calmar(results, min_date, max_date, start_balance),
|
||||||
'profit_factor': profit_factor,
|
'profit_factor': profit_factor,
|
||||||
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
|
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'backtest_start_ts': int(min_date.timestamp() * 1000),
|
'backtest_start_ts': int(min_date.timestamp() * 1000),
|
||||||
@ -785,8 +790,13 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||||
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
||||||
|
('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
|
||||||
|
('Sharpe', f"{strat_results['sharpe']:.2f}" if 'sharpe' in strat_results else 'N/A'),
|
||||||
|
('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'),
|
||||||
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
|
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
|
||||||
in strat_results else 'N/A'),
|
in strat_results else 'N/A'),
|
||||||
|
('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy'
|
||||||
|
in strat_results else 'N/A'),
|
||||||
('Trades per day', strat_results['trades_per_day']),
|
('Trades per day', strat_results['trades_per_day']),
|
||||||
('Avg. daily profit %',
|
('Avg. daily profit %',
|
||||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||||
|
@ -109,11 +109,10 @@ def migrate_trades_and_orders_table(
|
|||||||
else:
|
else:
|
||||||
is_short = get_column_def(cols, 'is_short', '0')
|
is_short = get_column_def(cols, 'is_short', '0')
|
||||||
|
|
||||||
# Margin Properties
|
# Futures Properties
|
||||||
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
|
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
|
||||||
|
|
||||||
# Futures properties
|
|
||||||
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
|
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
|
||||||
|
max_stake_amount = get_column_def(cols, 'max_stake_amount', 'stake_amount')
|
||||||
|
|
||||||
# If ticker-interval existed use that, else null.
|
# If ticker-interval existed use that, else null.
|
||||||
if has_column(cols, 'ticker_interval'):
|
if has_column(cols, 'ticker_interval'):
|
||||||
@ -162,7 +161,8 @@ def migrate_trades_and_orders_table(
|
|||||||
timeframe, open_trade_value, close_profit_abs,
|
timeframe, open_trade_value, close_profit_abs,
|
||||||
trading_mode, leverage, liquidation_price, is_short,
|
trading_mode, leverage, liquidation_price, is_short,
|
||||||
interest_rate, funding_fees, realized_profit,
|
interest_rate, funding_fees, realized_profit,
|
||||||
amount_precision, price_precision, precision_mode, contract_size
|
amount_precision, price_precision, precision_mode, contract_size,
|
||||||
|
max_stake_amount
|
||||||
)
|
)
|
||||||
select id, lower(exchange), pair, {base_currency} base_currency,
|
select id, lower(exchange), pair, {base_currency} base_currency,
|
||||||
{stake_currency} stake_currency,
|
{stake_currency} stake_currency,
|
||||||
@ -190,7 +190,8 @@ def migrate_trades_and_orders_table(
|
|||||||
{is_short} is_short, {interest_rate} interest_rate,
|
{is_short} is_short, {interest_rate} interest_rate,
|
||||||
{funding_fees} funding_fees, {realized_profit} realized_profit,
|
{funding_fees} funding_fees, {realized_profit} realized_profit,
|
||||||
{amount_precision} amount_precision, {price_precision} price_precision,
|
{amount_precision} amount_precision, {price_precision} price_precision,
|
||||||
{precision_mode} precision_mode, {contract_size} contract_size
|
{precision_mode} precision_mode, {contract_size} contract_size,
|
||||||
|
{max_stake_amount} max_stake_amount
|
||||||
from {trade_back_name}
|
from {trade_back_name}
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
@ -310,8 +311,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||||||
# if ('orders' not in previous_tables
|
# if ('orders' not in previous_tables
|
||||||
# or not has_column(cols_orders, 'funding_fee')):
|
# or not has_column(cols_orders, 'funding_fee')):
|
||||||
migrating = False
|
migrating = False
|
||||||
# if not has_column(cols_trades, 'contract_size'):
|
# if not has_column(cols_orders, 'funding_fee'):
|
||||||
if not has_column(cols_orders, 'funding_fee'):
|
if not has_column(cols_trades, 'max_stake_amount'):
|
||||||
migrating = True
|
migrating = True
|
||||||
logger.info(f"Running database migration for trades - "
|
logger.info(f"Running database migration for trades - "
|
||||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||||
|
@ -293,6 +293,7 @@ class LocalTrade():
|
|||||||
close_profit: Optional[float] = None
|
close_profit: Optional[float] = None
|
||||||
close_profit_abs: Optional[float] = None
|
close_profit_abs: Optional[float] = None
|
||||||
stake_amount: float = 0.0
|
stake_amount: float = 0.0
|
||||||
|
max_stake_amount: float = 0.0
|
||||||
amount: float = 0.0
|
amount: float = 0.0
|
||||||
amount_requested: Optional[float] = None
|
amount_requested: Optional[float] = None
|
||||||
open_date: datetime
|
open_date: datetime
|
||||||
@ -469,8 +470,8 @@ class LocalTrade():
|
|||||||
'amount': round(self.amount, 8),
|
'amount': round(self.amount, 8),
|
||||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||||
'stake_amount': round(self.stake_amount, 8),
|
'stake_amount': round(self.stake_amount, 8),
|
||||||
|
'max_stake_amount': round(self.max_stake_amount, 8) if self.max_stake_amount else None,
|
||||||
'strategy': self.strategy,
|
'strategy': self.strategy,
|
||||||
'buy_tag': self.enter_tag,
|
|
||||||
'enter_tag': self.enter_tag,
|
'enter_tag': self.enter_tag,
|
||||||
'timeframe': self.timeframe,
|
'timeframe': self.timeframe,
|
||||||
|
|
||||||
@ -507,7 +508,6 @@ class LocalTrade():
|
|||||||
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||||
'profit_abs': self.close_profit_abs,
|
'profit_abs': self.close_profit_abs,
|
||||||
|
|
||||||
'sell_reason': self.exit_reason, # Deprecated
|
|
||||||
'exit_reason': self.exit_reason,
|
'exit_reason': self.exit_reason,
|
||||||
'exit_order_status': self.exit_order_status,
|
'exit_order_status': self.exit_order_status,
|
||||||
'stop_loss_abs': self.stop_loss,
|
'stop_loss_abs': self.stop_loss,
|
||||||
@ -876,6 +876,7 @@ class LocalTrade():
|
|||||||
ZERO = FtPrecise(0.0)
|
ZERO = FtPrecise(0.0)
|
||||||
current_amount = FtPrecise(0.0)
|
current_amount = FtPrecise(0.0)
|
||||||
current_stake = FtPrecise(0.0)
|
current_stake = FtPrecise(0.0)
|
||||||
|
max_stake_amount = FtPrecise(0.0)
|
||||||
total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
|
total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
|
||||||
avg_price = FtPrecise(0.0)
|
avg_price = FtPrecise(0.0)
|
||||||
close_profit = 0.0
|
close_profit = 0.0
|
||||||
@ -917,7 +918,9 @@ class LocalTrade():
|
|||||||
exit_rate, amount=exit_amount, open_rate=avg_price)
|
exit_rate, amount=exit_amount, open_rate=avg_price)
|
||||||
else:
|
else:
|
||||||
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
|
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
|
||||||
|
max_stake_amount += (tmp_amount * price)
|
||||||
self.funding_fees = funding_fees
|
self.funding_fees = funding_fees
|
||||||
|
self.max_stake_amount = float(max_stake_amount)
|
||||||
|
|
||||||
if close_profit:
|
if close_profit:
|
||||||
self.close_profit = close_profit
|
self.close_profit = close_profit
|
||||||
@ -1169,6 +1172,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
close_profit = Column(Float)
|
close_profit = Column(Float)
|
||||||
close_profit_abs = Column(Float)
|
close_profit_abs = Column(Float)
|
||||||
stake_amount = Column(Float, nullable=False)
|
stake_amount = Column(Float, nullable=False)
|
||||||
|
max_stake_amount = Column(Float)
|
||||||
amount = Column(Float)
|
amount = Column(Float)
|
||||||
amount_requested = Column(Float)
|
amount_requested = Column(Float)
|
||||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
@ -135,7 +135,7 @@ class VolumePairList(IPairList):
|
|||||||
filtered_tickers = [
|
filtered_tickers = [
|
||||||
v for k, v in tickers.items()
|
v for k, v in tickers.items()
|
||||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||||
and (self._use_range or v[self._sort_key] is not None)
|
and (self._use_range or v.get(self._sort_key) is not None)
|
||||||
and v['symbol'] in _pairlist)]
|
and v['symbol'] in _pairlist)]
|
||||||
pairlist = [s['symbol'] for s in filtered_tickers]
|
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||||
else:
|
else:
|
||||||
|
@ -217,8 +217,8 @@ class TradeSchema(BaseModel):
|
|||||||
amount: float
|
amount: float
|
||||||
amount_requested: float
|
amount_requested: float
|
||||||
stake_amount: float
|
stake_amount: float
|
||||||
|
max_stake_amount: Optional[float]
|
||||||
strategy: str
|
strategy: str
|
||||||
buy_tag: Optional[str] # Deprecated
|
|
||||||
enter_tag: Optional[str]
|
enter_tag: Optional[str]
|
||||||
timeframe: int
|
timeframe: int
|
||||||
fee_open: Optional[float]
|
fee_open: Optional[float]
|
||||||
@ -243,7 +243,6 @@ class TradeSchema(BaseModel):
|
|||||||
profit_pct: Optional[float]
|
profit_pct: Optional[float]
|
||||||
profit_abs: Optional[float]
|
profit_abs: Optional[float]
|
||||||
profit_fiat: Optional[float]
|
profit_fiat: Optional[float]
|
||||||
sell_reason: Optional[str] # Deprecated
|
|
||||||
exit_reason: Optional[str]
|
exit_reason: Optional[str]
|
||||||
exit_order_status: Optional[str]
|
exit_order_status: Optional[str]
|
||||||
stop_loss_abs: Optional[float]
|
stop_loss_abs: Optional[float]
|
||||||
|
@ -27,7 +27,7 @@ class FreqaiExampleStrategy(IStrategy):
|
|||||||
plot_config = {
|
plot_config = {
|
||||||
"main_plot": {},
|
"main_plot": {},
|
||||||
"subplots": {
|
"subplots": {
|
||||||
"prediction": {"prediction": {"color": "blue"}},
|
"&-s_close": {"prediction": {"color": "blue"}},
|
||||||
"do_predict": {
|
"do_predict": {
|
||||||
"do_predict": {"color": "brown"},
|
"do_predict": {"color": "brown"},
|
||||||
},
|
},
|
||||||
@ -184,7 +184,8 @@ class FreqaiExampleStrategy(IStrategy):
|
|||||||
# If user wishes to use multiple targets, they can add more by
|
# If user wishes to use multiple targets, they can add more by
|
||||||
# appending more columns with '&'. User should keep in mind that multi targets
|
# appending more columns with '&'. User should keep in mind that multi targets
|
||||||
# requires a multioutput prediction model such as
|
# requires a multioutput prediction model such as
|
||||||
# templates/CatboostPredictionMultiModel.py,
|
# freqai/prediction_models/CatboostRegressorMultiTarget.py,
|
||||||
|
# freqtrade trade --freqaimodel CatboostRegressorMultiTarget
|
||||||
|
|
||||||
# df["&-s_range"] = (
|
# df["&-s_range"] = (
|
||||||
# df["close"]
|
# df["close"]
|
||||||
|
@ -10,24 +10,24 @@ coveralls==3.3.1
|
|||||||
flake8==6.0.0
|
flake8==6.0.0
|
||||||
flake8-tidy-imports==4.8.0
|
flake8-tidy-imports==4.8.0
|
||||||
mypy==0.991
|
mypy==0.991
|
||||||
pre-commit==2.20.0
|
pre-commit==2.21.0
|
||||||
pytest==7.2.0
|
pytest==7.2.0
|
||||||
pytest-asyncio==0.20.3
|
pytest-asyncio==0.20.3
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.0.0
|
||||||
pytest-mock==3.10.0
|
pytest-mock==3.10.0
|
||||||
pytest-random-order==1.1.0
|
pytest-random-order==1.1.0
|
||||||
isort==5.11.3
|
isort==5.11.4
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.8.2
|
time-machine==2.9.0
|
||||||
# fastapi testing
|
# fastapi testing
|
||||||
httpx==0.23.1
|
httpx==0.23.1
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.2.6
|
nbconvert==7.2.7
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.2.1
|
types-cachetools==5.2.1
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.28.11.5
|
types-requests==2.28.11.7
|
||||||
types-tabulate==0.9.0.0
|
types-tabulate==0.9.0.0
|
||||||
types-python-dateutil==2.8.19.5
|
types-python-dateutil==2.8.19.5
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
scipy==1.9.3
|
scipy==1.9.3
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.1.3
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.8.2
|
filelock==3.9.0
|
||||||
progressbar2==4.2.0
|
progressbar2==4.2.0
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
numpy==1.23.5
|
numpy==1.24.1
|
||||||
pandas==1.5.2
|
pandas==1.5.2
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==2.4.27
|
ccxt==2.4.60
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==38.0.1; platform_machine == 'armv7l'
|
cryptography==38.0.1; platform_machine == 'armv7l'
|
||||||
cryptography==38.0.4; platform_machine != 'armv7l'
|
cryptography==38.0.4; platform_machine != 'armv7l'
|
||||||
@ -19,7 +19,7 @@ technical==1.3.0
|
|||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
tables==3.7.0
|
tables==3.8.0
|
||||||
blosc==1.11.1
|
blosc==1.11.1
|
||||||
joblib==1.2.0
|
joblib==1.2.0
|
||||||
pyarrow==10.0.1; platform_machine != 'armv7l'
|
pyarrow==10.0.1; platform_machine != 'armv7l'
|
||||||
@ -37,7 +37,7 @@ sdnotify==0.3.2
|
|||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.88.0
|
fastapi==0.88.0
|
||||||
pydantic==1.10.2
|
pydantic==1.10.4
|
||||||
uvicorn==0.20.0
|
uvicorn==0.20.0
|
||||||
pyjwt==2.6.0
|
pyjwt==2.6.0
|
||||||
aiofiles==22.1.0
|
aiofiles==22.1.0
|
||||||
|
10
setup.py
10
setup.py
@ -25,6 +25,11 @@ freqai_rl = [
|
|||||||
'sb3-contrib'
|
'sb3-contrib'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
hdf5 = [
|
||||||
|
'tables',
|
||||||
|
'blosc',
|
||||||
|
]
|
||||||
|
|
||||||
develop = [
|
develop = [
|
||||||
'coveralls',
|
'coveralls',
|
||||||
'flake8',
|
'flake8',
|
||||||
@ -44,7 +49,7 @@ jupyter = [
|
|||||||
'nbconvert',
|
'nbconvert',
|
||||||
]
|
]
|
||||||
|
|
||||||
all_extra = plot + develop + jupyter + hyperopt + freqai + freqai_rl
|
all_extra = plot + develop + jupyter + hyperopt + hdf5 + freqai + freqai_rl
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
tests_require=[
|
tests_require=[
|
||||||
@ -78,8 +83,6 @@ setup(
|
|||||||
'prompt-toolkit',
|
'prompt-toolkit',
|
||||||
'numpy',
|
'numpy',
|
||||||
'pandas',
|
'pandas',
|
||||||
'tables',
|
|
||||||
'blosc',
|
|
||||||
'joblib>=1.2.0',
|
'joblib>=1.2.0',
|
||||||
'pyarrow; platform_machine != "armv7l"',
|
'pyarrow; platform_machine != "armv7l"',
|
||||||
'fastapi',
|
'fastapi',
|
||||||
@ -97,6 +100,7 @@ setup(
|
|||||||
'plot': plot,
|
'plot': plot,
|
||||||
'jupyter': jupyter,
|
'jupyter': jupyter,
|
||||||
'hyperopt': hyperopt,
|
'hyperopt': hyperopt,
|
||||||
|
'hdf5': hdf5,
|
||||||
'freqai': freqai,
|
'freqai': freqai,
|
||||||
'freqai_rl': freqai_rl,
|
'freqai_rl': freqai_rl,
|
||||||
'all': all_extra,
|
'all': all_extra,
|
||||||
|
@ -1529,7 +1529,7 @@ def test_backtesting_show(mocker, testdatadir, capsys):
|
|||||||
args = [
|
args = [
|
||||||
"backtesting-show",
|
"backtesting-show",
|
||||||
"--export-filename",
|
"--export-filename",
|
||||||
f"{testdatadir / 'backtest_results/backtest-result_new.json'}",
|
f"{testdatadir / 'backtest_results/backtest-result.json'}",
|
||||||
"--show-pair-list"
|
"--show-pair-list"
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
|
@ -12,9 +12,11 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis
|
|||||||
get_latest_hyperopt_file, load_backtest_data,
|
get_latest_hyperopt_file, load_backtest_data,
|
||||||
load_backtest_metadata, load_trades, load_trades_from_db)
|
load_backtest_metadata, load_trades, load_trades_from_db)
|
||||||
from freqtrade.data.history import load_data, load_pair_history
|
from freqtrade.data.history import load_data, load_pair_history
|
||||||
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
|
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||||
calculate_max_drawdown, calculate_underwater,
|
calculate_expectancy, calculate_market_change,
|
||||||
combine_dataframes_with_mean, create_cum_profit)
|
calculate_max_drawdown, calculate_sharpe, calculate_sortino,
|
||||||
|
calculate_underwater, combine_dataframes_with_mean,
|
||||||
|
create_cum_profit)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
|
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
|
||||||
from tests.conftest_trades import MOCK_TRADE_COUNT
|
from tests.conftest_trades import MOCK_TRADE_COUNT
|
||||||
@ -30,10 +32,10 @@ def test_get_latest_backtest_filename(testdatadir, mocker):
|
|||||||
|
|
||||||
testdir_bt = testdatadir / "backtest_results"
|
testdir_bt = testdatadir / "backtest_results"
|
||||||
res = get_latest_backtest_filename(testdir_bt)
|
res = get_latest_backtest_filename(testdir_bt)
|
||||||
assert res == 'backtest-result_new.json'
|
assert res == 'backtest-result.json'
|
||||||
|
|
||||||
res = get_latest_backtest_filename(str(testdir_bt))
|
res = get_latest_backtest_filename(str(testdir_bt))
|
||||||
assert res == 'backtest-result_new.json'
|
assert res == 'backtest-result.json'
|
||||||
|
|
||||||
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
|
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
|
||||||
|
|
||||||
@ -81,7 +83,7 @@ def test_load_backtest_data_old_format(testdatadir, mocker):
|
|||||||
|
|
||||||
def test_load_backtest_data_new_format(testdatadir):
|
def test_load_backtest_data_new_format(testdatadir):
|
||||||
|
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
assert isinstance(bt_data, DataFrame)
|
assert isinstance(bt_data, DataFrame)
|
||||||
assert set(bt_data.columns) == set(BT_DATA_COLUMNS)
|
assert set(bt_data.columns) == set(BT_DATA_COLUMNS)
|
||||||
@ -182,7 +184,7 @@ def test_extract_trades_of_period(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_analyze_trade_parallelism(testdatadir):
|
def test_analyze_trade_parallelism(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
res = analyze_trade_parallelism(bt_data, "5m")
|
res = analyze_trade_parallelism(bt_data, "5m")
|
||||||
@ -256,7 +258,7 @@ def test_combine_dataframes_with_mean_no_data(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_create_cum_profit(testdatadir):
|
def test_create_cum_profit(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
|
|
||||||
@ -268,11 +270,11 @@ def test_create_cum_profit(testdatadir):
|
|||||||
"cum_profits", timeframe="5m")
|
"cum_profits", timeframe="5m")
|
||||||
assert "cum_profits" in cum_profits.columns
|
assert "cum_profits" in cum_profits.columns
|
||||||
assert cum_profits.iloc[0]['cum_profits'] == 0
|
assert cum_profits.iloc[0]['cum_profits'] == 0
|
||||||
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06
|
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
|
||||||
|
|
||||||
|
|
||||||
def test_create_cum_profit1(testdatadir):
|
def test_create_cum_profit1(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
# Move close-time to "off" the candle, to make sure the logic still works
|
# Move close-time to "off" the candle, to make sure the logic still works
|
||||||
bt_data['close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
|
bt_data['close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
|
||||||
@ -286,7 +288,7 @@ def test_create_cum_profit1(testdatadir):
|
|||||||
"cum_profits", timeframe="5m")
|
"cum_profits", timeframe="5m")
|
||||||
assert "cum_profits" in cum_profits.columns
|
assert "cum_profits" in cum_profits.columns
|
||||||
assert cum_profits.iloc[0]['cum_profits'] == 0
|
assert cum_profits.iloc[0]['cum_profits'] == 0
|
||||||
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06
|
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
|
||||||
|
|
||||||
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
with pytest.raises(ValueError, match='Trade dataframe empty.'):
|
||||||
create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'],
|
create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'],
|
||||||
@ -294,18 +296,18 @@ def test_create_cum_profit1(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_calculate_max_drawdown(testdatadir):
|
def test_calculate_max_drawdown(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
|
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
|
||||||
bt_data, value_col="profit_abs")
|
bt_data, value_col="profit_abs")
|
||||||
assert isinstance(drawdown, float)
|
assert isinstance(drawdown, float)
|
||||||
assert pytest.approx(drawdown) == 0.12071099
|
assert pytest.approx(drawdown) == 0.29753914
|
||||||
assert isinstance(hdate, Timestamp)
|
assert isinstance(hdate, Timestamp)
|
||||||
assert isinstance(lowdate, Timestamp)
|
assert isinstance(lowdate, Timestamp)
|
||||||
assert isinstance(hval, float)
|
assert isinstance(hval, float)
|
||||||
assert isinstance(lval, float)
|
assert isinstance(lval, float)
|
||||||
assert hdate == Timestamp('2018-01-25 01:30:00', tz='UTC')
|
assert hdate == Timestamp('2018-01-16 19:30:00', tz='UTC')
|
||||||
assert lowdate == Timestamp('2018-01-25 03:50:00', tz='UTC')
|
assert lowdate == Timestamp('2018-01-16 22:25:00', tz='UTC')
|
||||||
|
|
||||||
underwater = calculate_underwater(bt_data)
|
underwater = calculate_underwater(bt_data)
|
||||||
assert isinstance(underwater, DataFrame)
|
assert isinstance(underwater, DataFrame)
|
||||||
@ -318,14 +320,15 @@ def test_calculate_max_drawdown(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_calculate_csum(testdatadir):
|
def test_calculate_csum(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
csum_min, csum_max = calculate_csum(bt_data)
|
csum_min, csum_max = calculate_csum(bt_data)
|
||||||
|
|
||||||
assert isinstance(csum_min, float)
|
assert isinstance(csum_min, float)
|
||||||
assert isinstance(csum_max, float)
|
assert isinstance(csum_max, float)
|
||||||
assert csum_min < 0.01
|
assert csum_min < csum_max
|
||||||
assert csum_max > 0.02
|
assert csum_min < 0.0001
|
||||||
|
assert csum_max > 0.0002
|
||||||
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
|
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
|
||||||
|
|
||||||
assert csum_min1 == csum_min + 5
|
assert csum_min1 == csum_min + 5
|
||||||
@ -335,6 +338,69 @@ def test_calculate_csum(testdatadir):
|
|||||||
csum_min, csum_max = calculate_csum(DataFrame())
|
csum_min, csum_max = calculate_csum(DataFrame())
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_expectancy(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
expectancy = calculate_expectancy(DataFrame())
|
||||||
|
assert expectancy == 0.0
|
||||||
|
|
||||||
|
expectancy = calculate_expectancy(bt_data)
|
||||||
|
assert isinstance(expectancy, float)
|
||||||
|
assert pytest.approx(expectancy) == 0.07151374226574791
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_sortino(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
sortino = calculate_sortino(DataFrame(), None, None, 0)
|
||||||
|
assert sortino == 0.0
|
||||||
|
|
||||||
|
sortino = calculate_sortino(
|
||||||
|
bt_data,
|
||||||
|
bt_data['open_date'].min(),
|
||||||
|
bt_data['close_date'].max(),
|
||||||
|
0.01,
|
||||||
|
)
|
||||||
|
assert isinstance(sortino, float)
|
||||||
|
assert pytest.approx(sortino) == 35.17722
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_sharpe(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
sharpe = calculate_sharpe(DataFrame(), None, None, 0)
|
||||||
|
assert sharpe == 0.0
|
||||||
|
|
||||||
|
sharpe = calculate_sharpe(
|
||||||
|
bt_data,
|
||||||
|
bt_data['open_date'].min(),
|
||||||
|
bt_data['close_date'].max(),
|
||||||
|
0.01,
|
||||||
|
)
|
||||||
|
assert isinstance(sharpe, float)
|
||||||
|
assert pytest.approx(sharpe) == 44.5078669
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_calmar(testdatadir):
|
||||||
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
calmar = calculate_calmar(DataFrame(), None, None, 0)
|
||||||
|
assert calmar == 0.0
|
||||||
|
|
||||||
|
calmar = calculate_calmar(
|
||||||
|
bt_data,
|
||||||
|
bt_data['open_date'].min(),
|
||||||
|
bt_data['close_date'].max(),
|
||||||
|
0.01,
|
||||||
|
)
|
||||||
|
assert isinstance(calmar, float)
|
||||||
|
assert pytest.approx(calmar) == 559.040508
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('start,end,days, expected', [
|
@pytest.mark.parametrize('start,end,days, expected', [
|
||||||
(64900, 176000, 3 * 365, 0.3945),
|
(64900, 176000, 3 * 365, 0.3945),
|
||||||
(64900, 176000, 365, 1.7119),
|
(64900, 176000, 365, 1.7119),
|
||||||
|
@ -23,7 +23,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
|||||||
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'limit'
|
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
@ -8,16 +8,19 @@ suitable to run with freqtrade.
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.exchange.exchange import timeframe_to_msecs
|
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
from tests.conftest import get_default_conf_usdt
|
from tests.conftest import get_default_conf_usdt
|
||||||
|
|
||||||
|
|
||||||
|
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
|
||||||
|
|
||||||
# Exchanges that should be tested
|
# Exchanges that should be tested
|
||||||
EXCHANGES = {
|
EXCHANGES = {
|
||||||
'bittrex': {
|
'bittrex': {
|
||||||
@ -141,19 +144,19 @@ def exchange_futures(request, exchange_conf, class_mocker):
|
|||||||
@pytest.mark.longrun
|
@pytest.mark.longrun
|
||||||
class TestCCXTExchange():
|
class TestCCXTExchange():
|
||||||
|
|
||||||
def test_load_markets(self, exchange):
|
def test_load_markets(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
markets = exchange.markets
|
markets = exch.markets
|
||||||
assert pair in markets
|
assert pair in markets
|
||||||
assert isinstance(markets[pair], dict)
|
assert isinstance(markets[pair], dict)
|
||||||
assert exchange.market_is_spot(markets[pair])
|
assert exch.market_is_spot(markets[pair])
|
||||||
|
|
||||||
def test_has_validations(self, exchange):
|
def test_has_validations(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
|
|
||||||
exchange, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
|
|
||||||
exchange.validate_ordertypes({
|
exch.validate_ordertypes({
|
||||||
'entry': 'limit',
|
'entry': 'limit',
|
||||||
'exit': 'limit',
|
'exit': 'limit',
|
||||||
'stoploss': 'limit',
|
'stoploss': 'limit',
|
||||||
@ -162,13 +165,13 @@ class TestCCXTExchange():
|
|||||||
if exchangename == 'gateio':
|
if exchangename == 'gateio':
|
||||||
# gateio doesn't have market orders on spot
|
# gateio doesn't have market orders on spot
|
||||||
return
|
return
|
||||||
exchange.validate_ordertypes({
|
exch.validate_ordertypes({
|
||||||
'entry': 'market',
|
'entry': 'market',
|
||||||
'exit': 'market',
|
'exit': 'market',
|
||||||
'stoploss': 'market',
|
'stoploss': 'market',
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_load_markets_futures(self, exchange_futures):
|
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
@ -181,11 +184,11 @@ class TestCCXTExchange():
|
|||||||
|
|
||||||
assert exchange.market_is_future(markets[pair])
|
assert exchange.market_is_future(markets[pair])
|
||||||
|
|
||||||
def test_ccxt_fetch_tickers(self, exchange):
|
def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
|
|
||||||
tickers = exchange.get_tickers()
|
tickers = exch.get_tickers()
|
||||||
assert pair in tickers
|
assert pair in tickers
|
||||||
assert 'ask' in tickers[pair]
|
assert 'ask' in tickers[pair]
|
||||||
assert tickers[pair]['ask'] is not None
|
assert tickers[pair]['ask'] is not None
|
||||||
@ -195,11 +198,11 @@ class TestCCXTExchange():
|
|||||||
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
||||||
assert tickers[pair]['quoteVolume'] is not None
|
assert tickers[pair]['quoteVolume'] is not None
|
||||||
|
|
||||||
def test_ccxt_fetch_ticker(self, exchange):
|
def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
|
|
||||||
ticker = exchange.fetch_ticker(pair)
|
ticker = exch.fetch_ticker(pair)
|
||||||
assert 'ask' in ticker
|
assert 'ask' in ticker
|
||||||
assert ticker['ask'] is not None
|
assert ticker['ask'] is not None
|
||||||
assert 'bid' in ticker
|
assert 'bid' in ticker
|
||||||
@ -208,21 +211,21 @@ class TestCCXTExchange():
|
|||||||
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
if EXCHANGES[exchangename].get('hasQuoteVolume'):
|
||||||
assert ticker['quoteVolume'] is not None
|
assert ticker['quoteVolume'] is not None
|
||||||
|
|
||||||
def test_ccxt_fetch_l2_orderbook(self, exchange):
|
def test_ccxt_fetch_l2_orderbook(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
l2 = exchange.fetch_l2_order_book(pair)
|
l2 = exch.fetch_l2_order_book(pair)
|
||||||
assert 'asks' in l2
|
assert 'asks' in l2
|
||||||
assert 'bids' in l2
|
assert 'bids' in l2
|
||||||
assert len(l2['asks']) >= 1
|
assert len(l2['asks']) >= 1
|
||||||
assert len(l2['bids']) >= 1
|
assert len(l2['bids']) >= 1
|
||||||
l2_limit_range = exchange._ft_has['l2_limit_range']
|
l2_limit_range = exch._ft_has['l2_limit_range']
|
||||||
l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
|
l2_limit_range_required = exch._ft_has['l2_limit_range_required']
|
||||||
if exchangename == 'gateio':
|
if exchangename == 'gateio':
|
||||||
# TODO: Gateio is unstable here at the moment, ignoring the limit partially.
|
# TODO: Gateio is unstable here at the moment, ignoring the limit partially.
|
||||||
return
|
return
|
||||||
for val in [1, 2, 5, 25, 100]:
|
for val in [1, 2, 5, 25, 100]:
|
||||||
l2 = exchange.fetch_l2_order_book(pair, val)
|
l2 = exch.fetch_l2_order_book(pair, val)
|
||||||
if not l2_limit_range or val in l2_limit_range:
|
if not l2_limit_range or val in l2_limit_range:
|
||||||
if val > 50:
|
if val > 50:
|
||||||
# Orderbooks are not always this deep.
|
# Orderbooks are not always this deep.
|
||||||
@ -232,7 +235,7 @@ class TestCCXTExchange():
|
|||||||
assert len(l2['asks']) == val
|
assert len(l2['asks']) == val
|
||||||
assert len(l2['bids']) == val
|
assert len(l2['bids']) == val
|
||||||
else:
|
else:
|
||||||
next_limit = exchange.get_next_limit_in_list(
|
next_limit = exch.get_next_limit_in_list(
|
||||||
val, l2_limit_range, l2_limit_range_required)
|
val, l2_limit_range, l2_limit_range_required)
|
||||||
if next_limit is None:
|
if next_limit is None:
|
||||||
assert len(l2['asks']) > 100
|
assert len(l2['asks']) > 100
|
||||||
@ -245,23 +248,23 @@ class TestCCXTExchange():
|
|||||||
assert len(l2['asks']) == next_limit
|
assert len(l2['asks']) == next_limit
|
||||||
assert len(l2['asks']) == next_limit
|
assert len(l2['asks']) == next_limit
|
||||||
|
|
||||||
def test_ccxt_fetch_ohlcv(self, exchange):
|
def test_ccxt_fetch_ohlcv(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||||
|
|
||||||
pair_tf = (pair, timeframe, CandleType.SPOT)
|
pair_tf = (pair, timeframe, CandleType.SPOT)
|
||||||
|
|
||||||
ohlcv = exchange.refresh_latest_ohlcv([pair_tf])
|
ohlcv = exch.refresh_latest_ohlcv([pair_tf])
|
||||||
assert isinstance(ohlcv, dict)
|
assert isinstance(ohlcv, dict)
|
||||||
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
|
assert len(ohlcv[pair_tf]) == len(exch.klines(pair_tf))
|
||||||
# assert len(exchange.klines(pair_tf)) > 200
|
# assert len(exch.klines(pair_tf)) > 200
|
||||||
# Assume 90% uptime ...
|
# Assume 90% uptime ...
|
||||||
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(
|
assert len(exch.klines(pair_tf)) > exch.ohlcv_candle_limit(
|
||||||
timeframe, CandleType.SPOT) * 0.90
|
timeframe, CandleType.SPOT) * 0.90
|
||||||
# Check if last-timeframe is within the last 2 intervals
|
# Check if last-timeframe is within the last 2 intervals
|
||||||
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
||||||
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
||||||
|
|
||||||
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type):
|
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type):
|
||||||
|
|
||||||
@ -289,17 +292,17 @@ class TestCCXTExchange():
|
|||||||
assert len(candles) >= min(candle_count, candle_count1)
|
assert len(candles) >= min(candle_count, candle_count1)
|
||||||
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
||||||
|
|
||||||
def test_ccxt__async_get_candle_history(self, exchange):
|
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange
|
exc, exchangename = exchange
|
||||||
# For some weired reason, this test returns random lengths for bittrex.
|
# For some weired reason, this test returns random lengths for bittrex.
|
||||||
if not exchange._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'):
|
if not exc._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'):
|
||||||
return
|
return
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||||
self.ccxt__async_get_candle_history(
|
self.ccxt__async_get_candle_history(
|
||||||
exchange, exchangename, pair, timeframe, CandleType.SPOT)
|
exc, exchangename, pair, timeframe, CandleType.SPOT)
|
||||||
|
|
||||||
def test_ccxt__async_get_candle_history_futures(self, exchange_futures):
|
def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
@ -309,7 +312,7 @@ class TestCCXTExchange():
|
|||||||
self.ccxt__async_get_candle_history(
|
self.ccxt__async_get_candle_history(
|
||||||
exchange, exchangename, pair, timeframe, CandleType.FUTURES)
|
exchange, exchangename, pair, timeframe, CandleType.FUTURES)
|
||||||
|
|
||||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
@ -347,7 +350,7 @@ class TestCCXTExchange():
|
|||||||
(rate['open'].min() != rate['open'].max())
|
(rate['open'].min() != rate['open'].max())
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_ccxt_fetch_mark_price_history(self, exchange_futures):
|
def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
@ -371,7 +374,7 @@ class TestCCXTExchange():
|
|||||||
assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0
|
assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0
|
||||||
assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0
|
assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0
|
||||||
|
|
||||||
def test_ccxt__calculate_funding_fees(self, exchange_futures):
|
def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
@ -387,16 +390,16 @@ class TestCCXTExchange():
|
|||||||
|
|
||||||
# TODO: tests fetch_trades (?)
|
# TODO: tests fetch_trades (?)
|
||||||
|
|
||||||
def test_ccxt_get_fee(self, exchange):
|
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
threshold = 0.01
|
threshold = 0.01
|
||||||
assert 0 < exchange.get_fee(pair, 'limit', 'buy') < threshold
|
assert 0 < exch.get_fee(pair, 'limit', 'buy') < threshold
|
||||||
assert 0 < exchange.get_fee(pair, 'limit', 'sell') < threshold
|
assert 0 < exch.get_fee(pair, 'limit', 'sell') < threshold
|
||||||
assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold
|
assert 0 < exch.get_fee(pair, 'market', 'buy') < threshold
|
||||||
assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold
|
assert 0 < exch.get_fee(pair, 'market', 'sell') < threshold
|
||||||
|
|
||||||
def test_ccxt_get_max_leverage_spot(self, exchange):
|
def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
spot, spot_name = exchange
|
spot, spot_name = exchange
|
||||||
if spot:
|
if spot:
|
||||||
leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market')
|
leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market')
|
||||||
@ -406,7 +409,7 @@ class TestCCXTExchange():
|
|||||||
assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int))
|
assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int))
|
||||||
assert spot_leverage >= 1.0
|
assert spot_leverage >= 1.0
|
||||||
|
|
||||||
def test_ccxt_get_max_leverage_futures(self, exchange_futures):
|
def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures:
|
if futures:
|
||||||
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
||||||
@ -419,7 +422,7 @@ class TestCCXTExchange():
|
|||||||
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
|
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
|
||||||
assert futures_leverage >= 1.0
|
assert futures_leverage >= 1.0
|
||||||
|
|
||||||
def test_ccxt_get_contract_size(self, exchange_futures):
|
def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures:
|
if futures:
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
@ -430,7 +433,7 @@ class TestCCXTExchange():
|
|||||||
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
|
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
|
||||||
assert contract_size >= 0.0
|
assert contract_size >= 0.0
|
||||||
|
|
||||||
def test_ccxt_load_leverage_tiers(self, exchange_futures):
|
def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||||
leverage_tiers = futures.load_leverage_tiers()
|
leverage_tiers = futures.load_leverage_tiers()
|
||||||
@ -463,7 +466,7 @@ class TestCCXTExchange():
|
|||||||
oldminNotional = tier['minNotional']
|
oldminNotional = tier['minNotional']
|
||||||
oldmaxNotional = tier['maxNotional']
|
oldmaxNotional = tier['maxNotional']
|
||||||
|
|
||||||
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
|
def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||||
|
|
||||||
@ -494,7 +497,7 @@ class TestCCXTExchange():
|
|||||||
assert (isinstance(liquidation_price, float))
|
assert (isinstance(liquidation_price, float))
|
||||||
assert liquidation_price >= 0.0
|
assert liquidation_price >= 0.0
|
||||||
|
|
||||||
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures):
|
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures:
|
if futures:
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.leverage import interest
|
from freqtrade.leverage import interest
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
|
|
||||||
@ -29,3 +30,13 @@ def test_interest(exchange, interest_rate, hours, expected):
|
|||||||
rate=FtPrecise(interest_rate),
|
rate=FtPrecise(interest_rate),
|
||||||
hours=hours
|
hours=hours
|
||||||
))) == expected
|
))) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_interest_exception():
|
||||||
|
with pytest.raises(OperationalException, match=r"Leverage not available on .* with freqtrade"):
|
||||||
|
interest(
|
||||||
|
exchange_name='bitmex',
|
||||||
|
borrowed=FtPrecise(60.0),
|
||||||
|
rate=FtPrecise(0.0005),
|
||||||
|
hours=ten_mins
|
||||||
|
)
|
||||||
|
@ -710,6 +710,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
expected = pd.DataFrame(
|
expected = pd.DataFrame(
|
||||||
{'pair': [pair, pair],
|
{'pair': [pair, pair],
|
||||||
'stake_amount': [0.001, 0.001],
|
'stake_amount': [0.001, 0.001],
|
||||||
|
'max_stake_amount': [0.001, 0.001],
|
||||||
'amount': [0.00957442, 0.0097064],
|
'amount': [0.00957442, 0.0097064],
|
||||||
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||||
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
||||||
|
@ -50,6 +50,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
|||||||
expected = pd.DataFrame(
|
expected = pd.DataFrame(
|
||||||
{'pair': [pair, pair],
|
{'pair': [pair, pair],
|
||||||
'stake_amount': [500.0, 100.0],
|
'stake_amount': [500.0, 100.0],
|
||||||
|
'max_stake_amount': [500.0, 100],
|
||||||
'amount': [4806.87657523, 970.63960782],
|
'amount': [4806.87657523, 970.63960782],
|
||||||
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||||
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
||||||
|
@ -308,7 +308,7 @@ def test_generate_pair_metrics():
|
|||||||
|
|
||||||
def test_generate_daily_stats(testdatadir):
|
def test_generate_daily_stats(testdatadir):
|
||||||
|
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
res = generate_daily_stats(bt_data)
|
res = generate_daily_stats(bt_data)
|
||||||
assert isinstance(res, dict)
|
assert isinstance(res, dict)
|
||||||
@ -328,7 +328,7 @@ def test_generate_daily_stats(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_trading_stats(testdatadir):
|
def test_generate_trading_stats(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
res = generate_trading_stats(bt_data)
|
res = generate_trading_stats(bt_data)
|
||||||
assert isinstance(res, dict)
|
assert isinstance(res, dict)
|
||||||
@ -444,7 +444,7 @@ def test_generate_edge_table():
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_periodic_breakdown_stats(testdatadir):
|
def test_generate_periodic_breakdown_stats(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename).to_dict(orient='records')
|
bt_data = load_backtest_data(filename).to_dict(orient='records')
|
||||||
|
|
||||||
res = generate_periodic_breakdown_stats(bt_data, 'day')
|
res = generate_periodic_breakdown_stats(bt_data, 'day')
|
||||||
@ -472,7 +472,7 @@ def test__get_resample_from_period():
|
|||||||
|
|
||||||
|
|
||||||
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
|
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_stats(filename)
|
bt_data = load_backtest_stats(filename)
|
||||||
default_conf['backtest_show_pair_list'] = True
|
default_conf['backtest_show_pair_list'] = True
|
||||||
|
|
||||||
|
412
tests/persistence/test_migrations.py
Normal file
412
tests/persistence/test_migrations.py
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
|
||||||
|
from freqtrade.constants import DEFAULT_DB_PROD_URL
|
||||||
|
from freqtrade.enums import TradingMode
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.persistence import Trade, init_db
|
||||||
|
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
|
||||||
|
from freqtrade.persistence.models import PairLock
|
||||||
|
from tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
|
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_create_session(default_conf):
|
||||||
|
# Check if init create a session
|
||||||
|
init_db(default_conf['db_url'])
|
||||||
|
assert hasattr(Trade, '_session')
|
||||||
|
assert 'scoped_session' in type(Trade._session).__name__
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_custom_db_url(default_conf, tmpdir):
|
||||||
|
# Update path to a value other than default, but still in-memory
|
||||||
|
filename = f"{tmpdir}/freqtrade2_test.sqlite"
|
||||||
|
assert not Path(filename).is_file()
|
||||||
|
|
||||||
|
default_conf.update({'db_url': f'sqlite:///{filename}'})
|
||||||
|
|
||||||
|
init_db(default_conf['db_url'])
|
||||||
|
assert Path(filename).is_file()
|
||||||
|
r = Trade._session.execute(text("PRAGMA journal_mode"))
|
||||||
|
assert r.first() == ('wal',)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_invalid_db_url():
|
||||||
|
# Update path to a value other than default, but still in-memory
|
||||||
|
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||||
|
init_db('unknown:///some.url')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
|
||||||
|
init_db('sqlite:///')
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_prod_db(default_conf, mocker):
|
||||||
|
default_conf.update({'dry_run': False})
|
||||||
|
default_conf.update({'db_url': DEFAULT_DB_PROD_URL})
|
||||||
|
|
||||||
|
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
|
||||||
|
|
||||||
|
init_db(default_conf['db_url'])
|
||||||
|
assert create_engine_mock.call_count == 1
|
||||||
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_dryrun_db(default_conf, tmpdir):
|
||||||
|
filename = f"{tmpdir}/freqtrade2_prod.sqlite"
|
||||||
|
assert not Path(filename).is_file()
|
||||||
|
default_conf.update({
|
||||||
|
'dry_run': True,
|
||||||
|
'db_url': f'sqlite:///{filename}'
|
||||||
|
})
|
||||||
|
|
||||||
|
init_db(default_conf['db_url'])
|
||||||
|
assert Path(filename).is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
|
"""
|
||||||
|
Test Database migration (starting with new pairformat)
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
amount = 103.223
|
||||||
|
# Always create all columns apart from the last!
|
||||||
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
exchange VARCHAR NOT NULL,
|
||||||
|
pair VARCHAR NOT NULL,
|
||||||
|
is_open BOOLEAN NOT NULL,
|
||||||
|
fee FLOAT NOT NULL,
|
||||||
|
open_rate FLOAT,
|
||||||
|
close_rate FLOAT,
|
||||||
|
close_profit FLOAT,
|
||||||
|
stake_amount FLOAT NOT NULL,
|
||||||
|
amount FLOAT,
|
||||||
|
open_date DATETIME NOT NULL,
|
||||||
|
close_date DATETIME,
|
||||||
|
open_order_id VARCHAR,
|
||||||
|
stop_loss FLOAT,
|
||||||
|
initial_stop_loss FLOAT,
|
||||||
|
max_rate FLOAT,
|
||||||
|
sell_reason VARCHAR,
|
||||||
|
strategy VARCHAR,
|
||||||
|
ticker_interval INTEGER,
|
||||||
|
stoploss_order_id VARCHAR,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CHECK (is_open IN (0, 1))
|
||||||
|
);"""
|
||||||
|
create_table_order = """CREATE TABLE orders (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
ft_trade_id INTEGER,
|
||||||
|
ft_order_side VARCHAR(25) NOT NULL,
|
||||||
|
ft_pair VARCHAR(25) NOT NULL,
|
||||||
|
ft_is_open BOOLEAN NOT NULL,
|
||||||
|
order_id VARCHAR(255) NOT NULL,
|
||||||
|
status VARCHAR(255),
|
||||||
|
symbol VARCHAR(25),
|
||||||
|
order_type VARCHAR(50),
|
||||||
|
side VARCHAR(25),
|
||||||
|
price FLOAT,
|
||||||
|
amount FLOAT,
|
||||||
|
filled FLOAT,
|
||||||
|
remaining FLOAT,
|
||||||
|
cost FLOAT,
|
||||||
|
order_date DATETIME,
|
||||||
|
order_filled_date DATETIME,
|
||||||
|
order_update_date DATETIME,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);"""
|
||||||
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
||||||
|
open_rate, stake_amount, amount, open_date,
|
||||||
|
stop_loss, initial_stop_loss, max_rate, ticker_interval,
|
||||||
|
open_order_id, stoploss_order_id)
|
||||||
|
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
||||||
|
0.00258580, {stake}, {amount},
|
||||||
|
'2019-11-28 12:44:24.000000',
|
||||||
|
0.0, 0.0, 0.0, '5m',
|
||||||
|
'buy_order', 'dry_stop_order_id222')
|
||||||
|
""".format(fee=fee.return_value,
|
||||||
|
stake=default_conf.get("stake_amount"),
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
insert_orders = f"""
|
||||||
|
insert into orders (
|
||||||
|
ft_trade_id,
|
||||||
|
ft_order_side,
|
||||||
|
ft_pair,
|
||||||
|
ft_is_open,
|
||||||
|
order_id,
|
||||||
|
status,
|
||||||
|
symbol,
|
||||||
|
order_type,
|
||||||
|
side,
|
||||||
|
price,
|
||||||
|
amount,
|
||||||
|
filled,
|
||||||
|
remaining,
|
||||||
|
cost)
|
||||||
|
values (
|
||||||
|
1,
|
||||||
|
'buy',
|
||||||
|
'ETC/BTC',
|
||||||
|
0,
|
||||||
|
'dry_buy_order',
|
||||||
|
'closed',
|
||||||
|
'ETC/BTC',
|
||||||
|
'limit',
|
||||||
|
'buy',
|
||||||
|
0.00258580,
|
||||||
|
{amount},
|
||||||
|
{amount},
|
||||||
|
0,
|
||||||
|
{amount * 0.00258580}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
'buy',
|
||||||
|
'ETC/BTC',
|
||||||
|
1,
|
||||||
|
'dry_buy_order22',
|
||||||
|
'canceled',
|
||||||
|
'ETC/BTC',
|
||||||
|
'limit',
|
||||||
|
'buy',
|
||||||
|
0.00258580,
|
||||||
|
{amount},
|
||||||
|
{amount},
|
||||||
|
0,
|
||||||
|
{amount * 0.00258580}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
'stoploss',
|
||||||
|
'ETC/BTC',
|
||||||
|
1,
|
||||||
|
'dry_stop_order_id11X',
|
||||||
|
'canceled',
|
||||||
|
'ETC/BTC',
|
||||||
|
'limit',
|
||||||
|
'sell',
|
||||||
|
0.00258580,
|
||||||
|
{amount},
|
||||||
|
{amount},
|
||||||
|
0,
|
||||||
|
{amount * 0.00258580}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
'stoploss',
|
||||||
|
'ETC/BTC',
|
||||||
|
1,
|
||||||
|
'dry_stop_order_id222',
|
||||||
|
'open',
|
||||||
|
'ETC/BTC',
|
||||||
|
'limit',
|
||||||
|
'sell',
|
||||||
|
0.00258580,
|
||||||
|
{amount},
|
||||||
|
{amount},
|
||||||
|
0,
|
||||||
|
{amount * 0.00258580}
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
|
# Create table using the old format
|
||||||
|
with engine.begin() as connection:
|
||||||
|
connection.execute(text(create_table_old))
|
||||||
|
connection.execute(text(create_table_order))
|
||||||
|
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
|
||||||
|
connection.execute(text("create index ix_trades_pair on trades(pair)"))
|
||||||
|
connection.execute(text(insert_table_old))
|
||||||
|
connection.execute(text(insert_orders))
|
||||||
|
|
||||||
|
# fake previous backup
|
||||||
|
connection.execute(text("create table trades_bak as select * from trades"))
|
||||||
|
|
||||||
|
connection.execute(text("create table trades_bak1 as select * from trades"))
|
||||||
|
# Run init to test migration
|
||||||
|
init_db(default_conf['db_url'])
|
||||||
|
|
||||||
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
assert trade.fee_open == fee.return_value
|
||||||
|
assert trade.fee_close == fee.return_value
|
||||||
|
assert trade.open_rate_requested is None
|
||||||
|
assert trade.close_rate_requested is None
|
||||||
|
assert trade.is_open == 1
|
||||||
|
assert trade.amount == amount
|
||||||
|
assert trade.amount_requested == amount
|
||||||
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
|
assert trade.pair == "ETC/BTC"
|
||||||
|
assert trade.exchange == "binance"
|
||||||
|
assert trade.max_rate == 0.0
|
||||||
|
assert trade.min_rate is None
|
||||||
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.initial_stop_loss == 0.0
|
||||||
|
assert trade.exit_reason is None
|
||||||
|
assert trade.strategy is None
|
||||||
|
assert trade.timeframe == '5m'
|
||||||
|
assert trade.stoploss_order_id == 'dry_stop_order_id222'
|
||||||
|
assert trade.stoploss_last_update is None
|
||||||
|
assert log_has("trying trades_bak1", caplog)
|
||||||
|
assert log_has("trying trades_bak2", caplog)
|
||||||
|
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
|
||||||
|
caplog)
|
||||||
|
assert log_has("Database migration finished.", caplog)
|
||||||
|
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
|
||||||
|
trade.amount, trade.open_rate)
|
||||||
|
assert trade.close_profit_abs is None
|
||||||
|
assert trade.stake_amount == trade.max_stake_amount
|
||||||
|
|
||||||
|
orders = trade.orders
|
||||||
|
assert len(orders) == 4
|
||||||
|
assert orders[0].order_id == 'dry_buy_order'
|
||||||
|
assert orders[0].ft_order_side == 'buy'
|
||||||
|
|
||||||
|
assert orders[-1].order_id == 'dry_stop_order_id222'
|
||||||
|
assert orders[-1].ft_order_side == 'stoploss'
|
||||||
|
assert orders[-1].ft_is_open is True
|
||||||
|
|
||||||
|
assert orders[1].order_id == 'dry_buy_order22'
|
||||||
|
assert orders[1].ft_order_side == 'buy'
|
||||||
|
assert orders[1].ft_is_open is False
|
||||||
|
|
||||||
|
assert orders[2].order_id == 'dry_stop_order_id11X'
|
||||||
|
assert orders[2].ft_order_side == 'stoploss'
|
||||||
|
assert orders[2].ft_is_open is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_too_old(mocker, default_conf, fee, caplog):
|
||||||
|
"""
|
||||||
|
Test Database migration (starting with new pairformat)
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
amount = 103.223
|
||||||
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
exchange VARCHAR NOT NULL,
|
||||||
|
pair VARCHAR NOT NULL,
|
||||||
|
is_open BOOLEAN NOT NULL,
|
||||||
|
fee_open FLOAT NOT NULL,
|
||||||
|
fee_close FLOAT NOT NULL,
|
||||||
|
open_rate FLOAT,
|
||||||
|
close_rate FLOAT,
|
||||||
|
close_profit FLOAT,
|
||||||
|
stake_amount FLOAT NOT NULL,
|
||||||
|
amount FLOAT,
|
||||||
|
open_date DATETIME NOT NULL,
|
||||||
|
close_date DATETIME,
|
||||||
|
open_order_id VARCHAR,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CHECK (is_open IN (0, 1))
|
||||||
|
);"""
|
||||||
|
|
||||||
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
||||||
|
open_rate, stake_amount, amount, open_date)
|
||||||
|
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
||||||
|
0.00258580, {stake}, {amount},
|
||||||
|
'2019-11-28 12:44:24.000000')
|
||||||
|
""".format(fee=fee.return_value,
|
||||||
|
stake=default_conf.get("stake_amount"),
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
|
# Create table using the old format
|
||||||
|
with engine.begin() as connection:
|
||||||
|
connection.execute(text(create_table_old))
|
||||||
|
connection.execute(text(insert_table_old))
|
||||||
|
|
||||||
|
# Run init to test migration
|
||||||
|
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
|
||||||
|
init_db(default_conf['db_url'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_get_last_sequence_ids():
|
||||||
|
engine = MagicMock()
|
||||||
|
engine.begin = MagicMock()
|
||||||
|
engine.name = 'postgresql'
|
||||||
|
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
||||||
|
|
||||||
|
assert engine.begin.call_count == 2
|
||||||
|
engine.reset_mock()
|
||||||
|
engine.begin.reset_mock()
|
||||||
|
|
||||||
|
engine.name = 'somethingelse'
|
||||||
|
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
||||||
|
|
||||||
|
assert engine.begin.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_set_sequence_ids():
|
||||||
|
engine = MagicMock()
|
||||||
|
engine.begin = MagicMock()
|
||||||
|
engine.name = 'postgresql'
|
||||||
|
set_sequence_ids(engine, 22, 55, 5)
|
||||||
|
|
||||||
|
assert engine.begin.call_count == 1
|
||||||
|
engine.reset_mock()
|
||||||
|
engine.begin.reset_mock()
|
||||||
|
|
||||||
|
engine.name = 'somethingelse'
|
||||||
|
set_sequence_ids(engine, 22, 55, 6)
|
||||||
|
|
||||||
|
assert engine.begin.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
|
||||||
|
"""
|
||||||
|
Test Database migration (starting with new pairformat)
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
# Always create all columns apart from the last!
|
||||||
|
create_table_old = """CREATE TABLE pairlocks (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
pair VARCHAR(25) NOT NULL,
|
||||||
|
reason VARCHAR(255),
|
||||||
|
lock_time DATETIME NOT NULL,
|
||||||
|
lock_end_time DATETIME NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
create_index1 = "CREATE INDEX ix_pairlocks_pair ON pairlocks (pair)"
|
||||||
|
create_index2 = "CREATE INDEX ix_pairlocks_lock_end_time ON pairlocks (lock_end_time)"
|
||||||
|
create_index3 = "CREATE INDEX ix_pairlocks_active ON pairlocks (active)"
|
||||||
|
insert_table_old = """INSERT INTO pairlocks (
|
||||||
|
id, pair, reason, lock_time, lock_end_time, active)
|
||||||
|
VALUES (1, 'ETH/BTC', 'Auto lock', '2021-07-12 18:41:03', '2021-07-11 18:45:00', 1)
|
||||||
|
"""
|
||||||
|
insert_table_old2 = """INSERT INTO pairlocks (
|
||||||
|
id, pair, reason, lock_time, lock_end_time, active)
|
||||||
|
VALUES (2, '*', 'Lock all', '2021-07-12 18:41:03', '2021-07-12 19:00:00', 1)
|
||||||
|
"""
|
||||||
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
# Create table using the old format
|
||||||
|
with engine.begin() as connection:
|
||||||
|
connection.execute(text(create_table_old))
|
||||||
|
|
||||||
|
connection.execute(text(insert_table_old))
|
||||||
|
connection.execute(text(insert_table_old2))
|
||||||
|
connection.execute(text(create_index1))
|
||||||
|
connection.execute(text(create_index2))
|
||||||
|
connection.execute(text(create_index3))
|
||||||
|
|
||||||
|
init_db(default_conf['db_url'])
|
||||||
|
|
||||||
|
assert len(PairLock.query.all()) == 2
|
||||||
|
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
|
||||||
|
pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()
|
||||||
|
assert len(pairlocks) == 1
|
||||||
|
pairlocks[0].pair == 'ETH/BTC'
|
||||||
|
pairlocks[0].side == '*'
|
@ -1,78 +1,20 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine, text
|
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DB_PROD_URL
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.enums import TradingMode
|
from freqtrade.enums import TradingMode
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException
|
||||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||||
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
|
|
||||||
from freqtrade.persistence.models import PairLock
|
|
||||||
from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re
|
from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re
|
||||||
|
|
||||||
|
|
||||||
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
|
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
|
||||||
|
|
||||||
|
|
||||||
def test_init_create_session(default_conf):
|
|
||||||
# Check if init create a session
|
|
||||||
init_db(default_conf['db_url'])
|
|
||||||
assert hasattr(Trade, '_session')
|
|
||||||
assert 'scoped_session' in type(Trade._session).__name__
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_custom_db_url(default_conf, tmpdir):
|
|
||||||
# Update path to a value other than default, but still in-memory
|
|
||||||
filename = f"{tmpdir}/freqtrade2_test.sqlite"
|
|
||||||
assert not Path(filename).is_file()
|
|
||||||
|
|
||||||
default_conf.update({'db_url': f'sqlite:///{filename}'})
|
|
||||||
|
|
||||||
init_db(default_conf['db_url'])
|
|
||||||
assert Path(filename).is_file()
|
|
||||||
r = Trade._session.execute(text("PRAGMA journal_mode"))
|
|
||||||
assert r.first() == ('wal',)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_invalid_db_url():
|
|
||||||
# Update path to a value other than default, but still in-memory
|
|
||||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
|
||||||
init_db('unknown:///some.url')
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
|
|
||||||
init_db('sqlite:///')
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_prod_db(default_conf, mocker):
|
|
||||||
default_conf.update({'dry_run': False})
|
|
||||||
default_conf.update({'db_url': DEFAULT_DB_PROD_URL})
|
|
||||||
|
|
||||||
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
|
|
||||||
|
|
||||||
init_db(default_conf['db_url'])
|
|
||||||
assert create_engine_mock.call_count == 1
|
|
||||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_dryrun_db(default_conf, tmpdir):
|
|
||||||
filename = f"{tmpdir}/freqtrade2_prod.sqlite"
|
|
||||||
assert not Path(filename).is_file()
|
|
||||||
default_conf.update({
|
|
||||||
'dry_run': True,
|
|
||||||
'db_url': f'sqlite:///{filename}'
|
|
||||||
})
|
|
||||||
|
|
||||||
init_db(default_conf['db_url'])
|
|
||||||
assert Path(filename).is_file()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('is_short', [False, True])
|
@pytest.mark.parametrize('is_short', [False, True])
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_enter_exit_side(fee, is_short):
|
def test_enter_exit_side(fee, is_short):
|
||||||
@ -316,8 +258,7 @@ def test_interest(fee, exchange, is_short, lev, minutes, rate, interest,
|
|||||||
(True, 3.0, 30.0, margin),
|
(True, 3.0, 30.0, margin),
|
||||||
])
|
])
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee,
|
def test_borrowed(fee, is_short, lev, borrowed, trading_mode):
|
||||||
caplog, is_short, lev, borrowed, trading_mode):
|
|
||||||
"""
|
"""
|
||||||
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
|
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
|
||||||
fee: 0.25% quote
|
fee: 0.25% quote
|
||||||
@ -1204,347 +1145,6 @@ def test_calc_profit(
|
|||||||
trade.open_rate)) == round(profit_ratio, 8)
|
trade.open_rate)) == round(profit_ratio, 8)
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_new(mocker, default_conf, fee, caplog):
|
|
||||||
"""
|
|
||||||
Test Database migration (starting with new pairformat)
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
amount = 103.223
|
|
||||||
# Always create all columns apart from the last!
|
|
||||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
exchange VARCHAR NOT NULL,
|
|
||||||
pair VARCHAR NOT NULL,
|
|
||||||
is_open BOOLEAN NOT NULL,
|
|
||||||
fee FLOAT NOT NULL,
|
|
||||||
open_rate FLOAT,
|
|
||||||
close_rate FLOAT,
|
|
||||||
close_profit FLOAT,
|
|
||||||
stake_amount FLOAT NOT NULL,
|
|
||||||
amount FLOAT,
|
|
||||||
open_date DATETIME NOT NULL,
|
|
||||||
close_date DATETIME,
|
|
||||||
open_order_id VARCHAR,
|
|
||||||
stop_loss FLOAT,
|
|
||||||
initial_stop_loss FLOAT,
|
|
||||||
max_rate FLOAT,
|
|
||||||
sell_reason VARCHAR,
|
|
||||||
strategy VARCHAR,
|
|
||||||
ticker_interval INTEGER,
|
|
||||||
stoploss_order_id VARCHAR,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
CHECK (is_open IN (0, 1))
|
|
||||||
);"""
|
|
||||||
create_table_order = """CREATE TABLE orders (
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
ft_trade_id INTEGER,
|
|
||||||
ft_order_side VARCHAR(25) NOT NULL,
|
|
||||||
ft_pair VARCHAR(25) NOT NULL,
|
|
||||||
ft_is_open BOOLEAN NOT NULL,
|
|
||||||
order_id VARCHAR(255) NOT NULL,
|
|
||||||
status VARCHAR(255),
|
|
||||||
symbol VARCHAR(25),
|
|
||||||
order_type VARCHAR(50),
|
|
||||||
side VARCHAR(25),
|
|
||||||
price FLOAT,
|
|
||||||
amount FLOAT,
|
|
||||||
filled FLOAT,
|
|
||||||
remaining FLOAT,
|
|
||||||
cost FLOAT,
|
|
||||||
order_date DATETIME,
|
|
||||||
order_filled_date DATETIME,
|
|
||||||
order_update_date DATETIME,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
);"""
|
|
||||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
|
||||||
open_rate, stake_amount, amount, open_date,
|
|
||||||
stop_loss, initial_stop_loss, max_rate, ticker_interval,
|
|
||||||
open_order_id, stoploss_order_id)
|
|
||||||
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
|
||||||
0.00258580, {stake}, {amount},
|
|
||||||
'2019-11-28 12:44:24.000000',
|
|
||||||
0.0, 0.0, 0.0, '5m',
|
|
||||||
'buy_order', 'dry_stop_order_id222')
|
|
||||||
""".format(fee=fee.return_value,
|
|
||||||
stake=default_conf.get("stake_amount"),
|
|
||||||
amount=amount
|
|
||||||
)
|
|
||||||
insert_orders = f"""
|
|
||||||
insert into orders (
|
|
||||||
ft_trade_id,
|
|
||||||
ft_order_side,
|
|
||||||
ft_pair,
|
|
||||||
ft_is_open,
|
|
||||||
order_id,
|
|
||||||
status,
|
|
||||||
symbol,
|
|
||||||
order_type,
|
|
||||||
side,
|
|
||||||
price,
|
|
||||||
amount,
|
|
||||||
filled,
|
|
||||||
remaining,
|
|
||||||
cost)
|
|
||||||
values (
|
|
||||||
1,
|
|
||||||
'buy',
|
|
||||||
'ETC/BTC',
|
|
||||||
0,
|
|
||||||
'dry_buy_order',
|
|
||||||
'closed',
|
|
||||||
'ETC/BTC',
|
|
||||||
'limit',
|
|
||||||
'buy',
|
|
||||||
0.00258580,
|
|
||||||
{amount},
|
|
||||||
{amount},
|
|
||||||
0,
|
|
||||||
{amount * 0.00258580}
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
'buy',
|
|
||||||
'ETC/BTC',
|
|
||||||
1,
|
|
||||||
'dry_buy_order22',
|
|
||||||
'canceled',
|
|
||||||
'ETC/BTC',
|
|
||||||
'limit',
|
|
||||||
'buy',
|
|
||||||
0.00258580,
|
|
||||||
{amount},
|
|
||||||
{amount},
|
|
||||||
0,
|
|
||||||
{amount * 0.00258580}
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
'stoploss',
|
|
||||||
'ETC/BTC',
|
|
||||||
1,
|
|
||||||
'dry_stop_order_id11X',
|
|
||||||
'canceled',
|
|
||||||
'ETC/BTC',
|
|
||||||
'limit',
|
|
||||||
'sell',
|
|
||||||
0.00258580,
|
|
||||||
{amount},
|
|
||||||
{amount},
|
|
||||||
0,
|
|
||||||
{amount * 0.00258580}
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1,
|
|
||||||
'stoploss',
|
|
||||||
'ETC/BTC',
|
|
||||||
1,
|
|
||||||
'dry_stop_order_id222',
|
|
||||||
'open',
|
|
||||||
'ETC/BTC',
|
|
||||||
'limit',
|
|
||||||
'sell',
|
|
||||||
0.00258580,
|
|
||||||
{amount},
|
|
||||||
{amount},
|
|
||||||
0,
|
|
||||||
{amount * 0.00258580}
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
engine = create_engine('sqlite://')
|
|
||||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
|
||||||
|
|
||||||
# Create table using the old format
|
|
||||||
with engine.begin() as connection:
|
|
||||||
connection.execute(text(create_table_old))
|
|
||||||
connection.execute(text(create_table_order))
|
|
||||||
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
|
|
||||||
connection.execute(text("create index ix_trades_pair on trades(pair)"))
|
|
||||||
connection.execute(text(insert_table_old))
|
|
||||||
connection.execute(text(insert_orders))
|
|
||||||
|
|
||||||
# fake previous backup
|
|
||||||
connection.execute(text("create table trades_bak as select * from trades"))
|
|
||||||
|
|
||||||
connection.execute(text("create table trades_bak1 as select * from trades"))
|
|
||||||
# Run init to test migration
|
|
||||||
init_db(default_conf['db_url'])
|
|
||||||
|
|
||||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
|
||||||
trade = Trade.query.filter(Trade.id == 1).first()
|
|
||||||
assert trade.fee_open == fee.return_value
|
|
||||||
assert trade.fee_close == fee.return_value
|
|
||||||
assert trade.open_rate_requested is None
|
|
||||||
assert trade.close_rate_requested is None
|
|
||||||
assert trade.is_open == 1
|
|
||||||
assert trade.amount == amount
|
|
||||||
assert trade.amount_requested == amount
|
|
||||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
|
||||||
assert trade.pair == "ETC/BTC"
|
|
||||||
assert trade.exchange == "binance"
|
|
||||||
assert trade.max_rate == 0.0
|
|
||||||
assert trade.min_rate is None
|
|
||||||
assert trade.stop_loss == 0.0
|
|
||||||
assert trade.initial_stop_loss == 0.0
|
|
||||||
assert trade.exit_reason is None
|
|
||||||
assert trade.strategy is None
|
|
||||||
assert trade.timeframe == '5m'
|
|
||||||
assert trade.stoploss_order_id == 'dry_stop_order_id222'
|
|
||||||
assert trade.stoploss_last_update is None
|
|
||||||
assert log_has("trying trades_bak1", caplog)
|
|
||||||
assert log_has("trying trades_bak2", caplog)
|
|
||||||
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
|
|
||||||
caplog)
|
|
||||||
assert log_has("Database migration finished.", caplog)
|
|
||||||
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
|
|
||||||
trade.amount, trade.open_rate)
|
|
||||||
assert trade.close_profit_abs is None
|
|
||||||
|
|
||||||
orders = trade.orders
|
|
||||||
assert len(orders) == 4
|
|
||||||
assert orders[0].order_id == 'dry_buy_order'
|
|
||||||
assert orders[0].ft_order_side == 'buy'
|
|
||||||
|
|
||||||
assert orders[-1].order_id == 'dry_stop_order_id222'
|
|
||||||
assert orders[-1].ft_order_side == 'stoploss'
|
|
||||||
assert orders[-1].ft_is_open is True
|
|
||||||
|
|
||||||
assert orders[1].order_id == 'dry_buy_order22'
|
|
||||||
assert orders[1].ft_order_side == 'buy'
|
|
||||||
assert orders[1].ft_is_open is False
|
|
||||||
|
|
||||||
assert orders[2].order_id == 'dry_stop_order_id11X'
|
|
||||||
assert orders[2].ft_order_side == 'stoploss'
|
|
||||||
assert orders[2].ft_is_open is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_too_old(mocker, default_conf, fee, caplog):
|
|
||||||
"""
|
|
||||||
Test Database migration (starting with new pairformat)
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
amount = 103.223
|
|
||||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
exchange VARCHAR NOT NULL,
|
|
||||||
pair VARCHAR NOT NULL,
|
|
||||||
is_open BOOLEAN NOT NULL,
|
|
||||||
fee_open FLOAT NOT NULL,
|
|
||||||
fee_close FLOAT NOT NULL,
|
|
||||||
open_rate FLOAT,
|
|
||||||
close_rate FLOAT,
|
|
||||||
close_profit FLOAT,
|
|
||||||
stake_amount FLOAT NOT NULL,
|
|
||||||
amount FLOAT,
|
|
||||||
open_date DATETIME NOT NULL,
|
|
||||||
close_date DATETIME,
|
|
||||||
open_order_id VARCHAR,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
CHECK (is_open IN (0, 1))
|
|
||||||
);"""
|
|
||||||
|
|
||||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
|
||||||
open_rate, stake_amount, amount, open_date)
|
|
||||||
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
|
||||||
0.00258580, {stake}, {amount},
|
|
||||||
'2019-11-28 12:44:24.000000')
|
|
||||||
""".format(fee=fee.return_value,
|
|
||||||
stake=default_conf.get("stake_amount"),
|
|
||||||
amount=amount
|
|
||||||
)
|
|
||||||
engine = create_engine('sqlite://')
|
|
||||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
|
||||||
|
|
||||||
# Create table using the old format
|
|
||||||
with engine.begin() as connection:
|
|
||||||
connection.execute(text(create_table_old))
|
|
||||||
connection.execute(text(insert_table_old))
|
|
||||||
|
|
||||||
# Run init to test migration
|
|
||||||
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
|
|
||||||
init_db(default_conf['db_url'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_get_last_sequence_ids():
|
|
||||||
engine = MagicMock()
|
|
||||||
engine.begin = MagicMock()
|
|
||||||
engine.name = 'postgresql'
|
|
||||||
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
|
||||||
|
|
||||||
assert engine.begin.call_count == 2
|
|
||||||
engine.reset_mock()
|
|
||||||
engine.begin.reset_mock()
|
|
||||||
|
|
||||||
engine.name = 'somethingelse'
|
|
||||||
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
|
|
||||||
|
|
||||||
assert engine.begin.call_count == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_set_sequence_ids():
|
|
||||||
engine = MagicMock()
|
|
||||||
engine.begin = MagicMock()
|
|
||||||
engine.name = 'postgresql'
|
|
||||||
set_sequence_ids(engine, 22, 55, 5)
|
|
||||||
|
|
||||||
assert engine.begin.call_count == 1
|
|
||||||
engine.reset_mock()
|
|
||||||
engine.begin.reset_mock()
|
|
||||||
|
|
||||||
engine.name = 'somethingelse'
|
|
||||||
set_sequence_ids(engine, 22, 55, 6)
|
|
||||||
|
|
||||||
assert engine.begin.call_count == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
|
|
||||||
"""
|
|
||||||
Test Database migration (starting with new pairformat)
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
# Always create all columns apart from the last!
|
|
||||||
create_table_old = """CREATE TABLE pairlocks (
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
pair VARCHAR(25) NOT NULL,
|
|
||||||
reason VARCHAR(255),
|
|
||||||
lock_time DATETIME NOT NULL,
|
|
||||||
lock_end_time DATETIME NOT NULL,
|
|
||||||
active BOOLEAN NOT NULL,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
create_index1 = "CREATE INDEX ix_pairlocks_pair ON pairlocks (pair)"
|
|
||||||
create_index2 = "CREATE INDEX ix_pairlocks_lock_end_time ON pairlocks (lock_end_time)"
|
|
||||||
create_index3 = "CREATE INDEX ix_pairlocks_active ON pairlocks (active)"
|
|
||||||
insert_table_old = """INSERT INTO pairlocks (
|
|
||||||
id, pair, reason, lock_time, lock_end_time, active)
|
|
||||||
VALUES (1, 'ETH/BTC', 'Auto lock', '2021-07-12 18:41:03', '2021-07-11 18:45:00', 1)
|
|
||||||
"""
|
|
||||||
insert_table_old2 = """INSERT INTO pairlocks (
|
|
||||||
id, pair, reason, lock_time, lock_end_time, active)
|
|
||||||
VALUES (2, '*', 'Lock all', '2021-07-12 18:41:03', '2021-07-12 19:00:00', 1)
|
|
||||||
"""
|
|
||||||
engine = create_engine('sqlite://')
|
|
||||||
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
|
|
||||||
# Create table using the old format
|
|
||||||
with engine.begin() as connection:
|
|
||||||
connection.execute(text(create_table_old))
|
|
||||||
|
|
||||||
connection.execute(text(insert_table_old))
|
|
||||||
connection.execute(text(insert_table_old2))
|
|
||||||
connection.execute(text(create_index1))
|
|
||||||
connection.execute(text(create_index2))
|
|
||||||
connection.execute(text(create_index3))
|
|
||||||
|
|
||||||
init_db(default_conf['db_url'])
|
|
||||||
|
|
||||||
assert len(PairLock.query.all()) == 2
|
|
||||||
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
|
|
||||||
pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()
|
|
||||||
assert len(pairlocks) == 1
|
|
||||||
pairlocks[0].pair == 'ETH/BTC'
|
|
||||||
pairlocks[0].side == '*'
|
|
||||||
|
|
||||||
|
|
||||||
def test_adjust_stop_loss(fee):
|
def test_adjust_stop_loss(fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ADA/USDT',
|
pair='ADA/USDT',
|
||||||
@ -1758,6 +1358,7 @@ def test_to_json(fee):
|
|||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'amount_requested': 123.0,
|
'amount_requested': 123.0,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
|
'max_stake_amount': None,
|
||||||
'trade_duration': None,
|
'trade_duration': None,
|
||||||
'trade_duration_s': None,
|
'trade_duration_s': None,
|
||||||
'realized_profit': 0.0,
|
'realized_profit': 0.0,
|
||||||
@ -1767,7 +1368,6 @@ def test_to_json(fee):
|
|||||||
'profit_ratio': None,
|
'profit_ratio': None,
|
||||||
'profit_pct': None,
|
'profit_pct': None,
|
||||||
'profit_abs': None,
|
'profit_abs': None,
|
||||||
'sell_reason': None,
|
|
||||||
'exit_reason': None,
|
'exit_reason': None,
|
||||||
'exit_order_status': None,
|
'exit_order_status': None,
|
||||||
'stop_loss_abs': None,
|
'stop_loss_abs': None,
|
||||||
@ -1782,7 +1382,6 @@ def test_to_json(fee):
|
|||||||
'min_rate': None,
|
'min_rate': None,
|
||||||
'max_rate': None,
|
'max_rate': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'buy_tag': None,
|
|
||||||
'enter_tag': None,
|
'enter_tag': None,
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
@ -1826,6 +1425,7 @@ def test_to_json(fee):
|
|||||||
'amount': 100.0,
|
'amount': 100.0,
|
||||||
'amount_requested': 101.0,
|
'amount_requested': 101.0,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
|
'max_stake_amount': None,
|
||||||
'trade_duration': 60,
|
'trade_duration': 60,
|
||||||
'trade_duration_s': 3600,
|
'trade_duration_s': 3600,
|
||||||
'stop_loss_abs': None,
|
'stop_loss_abs': None,
|
||||||
@ -1857,11 +1457,9 @@ def test_to_json(fee):
|
|||||||
'open_order_id': None,
|
'open_order_id': None,
|
||||||
'open_rate_requested': None,
|
'open_rate_requested': None,
|
||||||
'open_trade_value': 12.33075,
|
'open_trade_value': 12.33075,
|
||||||
'sell_reason': None,
|
|
||||||
'exit_reason': None,
|
'exit_reason': None,
|
||||||
'exit_order_status': None,
|
'exit_order_status': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'buy_tag': 'buys_signal_001',
|
|
||||||
'enter_tag': 'buys_signal_001',
|
'enter_tag': 'buys_signal_001',
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
|
@ -46,13 +46,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
'open_trade_value': 0.0010025,
|
'open_trade_value': 0.0010025,
|
||||||
'close_rate_requested': ANY,
|
'close_rate_requested': ANY,
|
||||||
'sell_reason': ANY,
|
|
||||||
'exit_reason': ANY,
|
'exit_reason': ANY,
|
||||||
'exit_order_status': ANY,
|
'exit_order_status': ANY,
|
||||||
'min_rate': ANY,
|
'min_rate': ANY,
|
||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'strategy': ANY,
|
'strategy': ANY,
|
||||||
'buy_tag': ANY,
|
|
||||||
'enter_tag': ANY,
|
'enter_tag': ANY,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'open_order_id': ANY,
|
'open_order_id': ANY,
|
||||||
@ -64,6 +62,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'amount': 91.07468123,
|
'amount': 91.07468123,
|
||||||
'amount_requested': 91.07468124,
|
'amount_requested': 91.07468124,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
|
'max_stake_amount': ANY,
|
||||||
'trade_duration': None,
|
'trade_duration': None,
|
||||||
'trade_duration_s': None,
|
'trade_duration_s': None,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
|
@ -985,6 +985,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
'base_currency': 'ETH',
|
'base_currency': 'ETH',
|
||||||
'quote_currency': 'BTC',
|
'quote_currency': 'BTC',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
|
'max_stake_amount': ANY,
|
||||||
'stop_loss_abs': ANY,
|
'stop_loss_abs': ANY,
|
||||||
'stop_loss_pct': ANY,
|
'stop_loss_pct': ANY,
|
||||||
'stop_loss_ratio': ANY,
|
'stop_loss_ratio': ANY,
|
||||||
@ -1014,11 +1015,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
'open_order_id': open_order_id,
|
'open_order_id': open_order_id,
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
'open_trade_value': open_trade_value,
|
'open_trade_value': open_trade_value,
|
||||||
'sell_reason': None,
|
|
||||||
'exit_reason': None,
|
'exit_reason': None,
|
||||||
'exit_order_status': None,
|
'exit_order_status': None,
|
||||||
'strategy': CURRENT_TEST_STRATEGY,
|
'strategy': CURRENT_TEST_STRATEGY,
|
||||||
'buy_tag': None,
|
|
||||||
'enter_tag': None,
|
'enter_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
@ -1188,6 +1187,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
|||||||
'base_currency': 'ETH',
|
'base_currency': 'ETH',
|
||||||
'quote_currency': 'BTC',
|
'quote_currency': 'BTC',
|
||||||
'stake_amount': 1,
|
'stake_amount': 1,
|
||||||
|
'max_stake_amount': ANY,
|
||||||
'stop_loss_abs': None,
|
'stop_loss_abs': None,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': None,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_ratio': None,
|
||||||
@ -1218,11 +1218,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
|||||||
'open_order_id': '123456',
|
'open_order_id': '123456',
|
||||||
'open_rate_requested': None,
|
'open_rate_requested': None,
|
||||||
'open_trade_value': 0.24605460,
|
'open_trade_value': 0.24605460,
|
||||||
'sell_reason': None,
|
|
||||||
'exit_reason': None,
|
'exit_reason': None,
|
||||||
'exit_order_status': None,
|
'exit_order_status': None,
|
||||||
'strategy': CURRENT_TEST_STRATEGY,
|
'strategy': CURRENT_TEST_STRATEGY,
|
||||||
'buy_tag': None,
|
|
||||||
'enter_tag': None,
|
'enter_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
@ -1709,7 +1707,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
|
|||||||
mocker.patch('freqtrade.data.btanalysis._get_backtest_files',
|
mocker.patch('freqtrade.data.btanalysis._get_backtest_files',
|
||||||
return_value=[
|
return_value=[
|
||||||
testdatadir / 'backtest_results/backtest-result_multistrat.json',
|
testdatadir / 'backtest_results/backtest-result_multistrat.json',
|
||||||
testdatadir / 'backtest_results/backtest-result_new.json'
|
testdatadir / 'backtest_results/backtest-result.json'
|
||||||
])
|
])
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/backtest/history")
|
rc = client_get(client, f"{BASE_URI}/backtest/history")
|
||||||
|
@ -46,7 +46,7 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
|
|||||||
default_conf['trade_source'] = "file"
|
default_conf['trade_source'] = "file"
|
||||||
default_conf['timeframe'] = "5m"
|
default_conf['timeframe'] = "5m"
|
||||||
default_conf["datadir"] = testdatadir
|
default_conf["datadir"] = testdatadir
|
||||||
default_conf['exportfilename'] = testdatadir / "backtest-result_new.json"
|
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
||||||
supported_markets = ["TRX/BTC", "ADA/BTC"]
|
supported_markets = ["TRX/BTC", "ADA/BTC"]
|
||||||
ret = init_plotscript(default_conf, supported_markets)
|
ret = init_plotscript(default_conf, supported_markets)
|
||||||
assert "ohlcv" in ret
|
assert "ohlcv" in ret
|
||||||
@ -158,7 +158,7 @@ def test_plot_trades(testdatadir, caplog):
|
|||||||
assert fig == fig1
|
assert fig == fig1
|
||||||
assert log_has("No trades found.", caplog)
|
assert log_has("No trades found.", caplog)
|
||||||
pair = "ADA/BTC"
|
pair = "ADA/BTC"
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
trades = load_backtest_data(filename)
|
trades = load_backtest_data(filename)
|
||||||
trades = trades.loc[trades['pair'] == pair]
|
trades = trades.loc[trades['pair'] == pair]
|
||||||
|
|
||||||
@ -299,7 +299,7 @@ def test_generate_plot_file(mocker, caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_add_profit(testdatadir):
|
def test_add_profit(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
|
|
||||||
@ -319,7 +319,7 @@ def test_add_profit(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_profit_graph(testdatadir):
|
def test_generate_profit_graph(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||||
trades = load_backtest_data(filename)
|
trades = load_backtest_data(filename)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
pairs = ["TRX/BTC", "XLM/BTC"]
|
pairs = ["TRX/BTC", "XLM/BTC"]
|
||||||
@ -354,7 +354,7 @@ def test_generate_profit_graph(testdatadir):
|
|||||||
|
|
||||||
profit = find_trace_in_fig_data(figure.data, "Profit")
|
profit = find_trace_in_fig_data(figure.data, "Profit")
|
||||||
assert isinstance(profit, go.Scatter)
|
assert isinstance(profit, go.Scatter)
|
||||||
drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 35.69%")
|
drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 73.89%")
|
||||||
assert isinstance(drawdown, go.Scatter)
|
assert isinstance(drawdown, go.Scatter)
|
||||||
parallel = find_trace_in_fig_data(figure.data, "Parallel trades")
|
parallel = find_trace_in_fig_data(figure.data, "Parallel trades")
|
||||||
assert isinstance(parallel, go.Scatter)
|
assert isinstance(parallel, go.Scatter)
|
||||||
@ -395,7 +395,7 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
|
|||||||
|
|
||||||
default_conf['trade_source'] = 'file'
|
default_conf['trade_source'] = 'file'
|
||||||
default_conf["datadir"] = testdatadir
|
default_conf["datadir"] = testdatadir
|
||||||
default_conf['exportfilename'] = testdatadir / "backtest-result_new.json"
|
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
|
||||||
default_conf['indicators1'] = ["sma5", "ema10"]
|
default_conf['indicators1'] = ["sma5", "ema10"]
|
||||||
default_conf['indicators2'] = ["macd"]
|
default_conf['indicators2'] = ["macd"]
|
||||||
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
|
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
|
||||||
@ -466,7 +466,7 @@ def test_plot_profit(default_conf, mocker, testdatadir):
|
|||||||
match=r"No trades found, cannot generate Profit-plot.*"):
|
match=r"No trades found, cannot generate Profit-plot.*"):
|
||||||
plot_profit(default_conf)
|
plot_profit(default_conf)
|
||||||
|
|
||||||
default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result_new.json"
|
default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result.json"
|
||||||
|
|
||||||
plot_profit(default_conf)
|
plot_profit(default_conf)
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
{"latest_backtest":"backtest-result_new.json"}
|
{"latest_backtest":"backtest-result.json"}
|
||||||
|
1
tests/testdata/backtest_results/backtest-result.json
vendored
Normal file
1
tests/testdata/backtest_results/backtest-result.json
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user