Merge branch 'develop' of https://github.com/freqtrade/freqtrade into max-open-trades
This commit is contained in:
commit
ce661cb58b
@ -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.
|
||||||
|
@ -366,7 +366,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)
|
||||||
@ -401,7 +401,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
conditions.append(qtpylib.crossed_above(
|
conditions.append(qtpylib.crossed_above(
|
||||||
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
|
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
|
||||||
))
|
))
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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%}"),
|
||||||
|
@ -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:
|
||||||
|
@ -18,7 +18,7 @@ pytest-mock==3.10.0
|
|||||||
pytest-random-order==1.1.0
|
pytest-random-order==1.1.0
|
||||||
isort==5.11.4
|
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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
@ -336,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),
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user