Merge branch 'feat/short' into lev-freqtradebot

This commit is contained in:
Sam Germain 2021-10-13 19:02:57 -06:00
commit bcbe8f229c
53 changed files with 902 additions and 313 deletions

View File

@ -53,7 +53,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io
- [x] **Dry-run**: Run the bot without paying money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/).
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
- [x] **Manageable via Telegram**: Manage the bot with Telegram.
@ -71,7 +71,7 @@ cd freqtrade
./setup.sh --install
```
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/).
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/).
## Basic Usage

View File

@ -28,10 +28,8 @@
"name": "binance",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true},
"ccxt_config": {},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
},
"pair_whitelist": [
"ALGO/BTC",

View File

@ -28,11 +28,8 @@
"name": "ftx",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 50
},
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USD",
"ETH/USD",

View File

@ -84,12 +84,8 @@
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"password": "",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 500,
"aiohttp_trust_env": false
},
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"ALGO/BTC",
"ATOM/BTC",

View File

@ -28,10 +28,8 @@
"name": "kraken",
"key": "your_exchange_key",
"secret": "your_exchange_key",
"ccxt_config": {"enableRateLimit": true},
"ccxt_config": {},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 1000
},
"pair_whitelist": [
"ADA/EUR",

View File

@ -15,10 +15,10 @@ services:
volumes:
- "./user_data:/freqtrade/user_data"
# Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
# before enabling this.
# ports:
# - "127.0.0.1:8080:8080"
ports:
- "127.0.0.1:8080:8080"
# Default command used when running `docker compose up`
command: >
trade

View File

@ -447,45 +447,6 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
This is ongoing work. For now, it is supported only for binance and kucoin.
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
### Exchange configuration
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
However, the bot was tested by the development team with only Bittrex, Binance and Kraken,
so these are the only officially supported exchanges:
- [Bittrex](https://bittrex.com/): "bittrex"
- [Binance](https://www.binance.com/): "binance"
- [Kraken](https://kraken.com/): "kraken"
Feel free to test other exchanges and submit your PR to improve the bot.
Some exchanges require special configuration, which can be found on the [Exchange-specific Notes](exchanges.md) documentation page.
#### Sample exchange configuration
A exchange configuration for "binance" would look as follows:
```json
"exchange": {
"name": "binance",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
},
```
This configuration enables binance, as well as rate-limiting to avoid bans from the exchange.
`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
!!! Note
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
### What values can be used for fiat_display_currency?
The `fiat_display_currency` configuration parameter sets the base currency to use for the

View File

@ -70,6 +70,18 @@ docker-compose up -d
!!! Warning "Default configuration"
While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot.
#### Accessing the UI
If you've selected to enable FreqUI in the `new-config` step, you will have freqUI available at port `localhost:8080`.
You can now access the UI by typing localhost:8080 in your browser.
??? Note "UI Access on a remote servers"
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
Please also read the [API configuration with docker](rest-api.md#configuration-with-docker) section to learn more about this configuration.
#### Monitoring the bot
You can check for running instances with `docker-compose ps`.
@ -148,27 +160,9 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the
dockerfile: "./Dockerfile.<yourextension>"
```
You can then run `docker-compose build` to build the docker image, and run it using the commands described above.
You can then run `docker-compose build --pull` to build the docker image, and run it using the commands described above.
### Troubleshooting
#### Docker on Windows
* Error: `"Timestamp for this request is outside of the recvWindow."`
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
```
taskkill /IM "Docker Desktop.exe" /F
wsl --shutdown
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
```
!!! Warning
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
Best use a linux-VPS for running freqtrade reliably.
## Plotting with docker-compose
### Plotting with docker-compose
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
You can then use these commands as follows:
@ -179,7 +173,7 @@ docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p B
The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser.
## Data analysis using docker compose
### Data analysis using docker compose
Freqtrade provides a docker-compose file which starts up a jupyter lab server.
You can run this server using the following command:
@ -196,3 +190,22 @@ Since part of this image is built on your machine, it is recommended to rebuild
``` bash
docker-compose -f docker/docker-compose-jupyter.yml build --no-cache
```
## Troubleshooting
### Docker on Windows
* Error: `"Timestamp for this request is outside of the recvWindow."`
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
``` bash
taskkill /IM "Docker Desktop.exe" /F
wsl --shutdown
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
```
!!! Warning
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
Best use a linux-VPS for running freqtrade reliably.

View File

@ -2,6 +2,56 @@
This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges.
## Exchange configuration
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
However, the bot was tested by the development team with only a few exchanges.
A current list of these can be found in the "Home" section of this documentation.
Feel free to test other exchanges and submit your feedback or PR to improve the bot or confirm exchanges that work flawlessly..
Some exchanges require special configuration, which can be found below.
### Sample exchange configuration
A exchange configuration for "binance" would look as follows:
```json
"exchange": {
"name": "binance",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {},
// ...
```
### Setting rate limits
Usually, rate limits set by CCXT are reliable and work well.
In case of problems related to rate-limits (usually DDOS Exceptions in your logs), it's easy to change rateLimit settings to other values.
```json
"exchange": {
"name": "kraken",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 3100
},
```
This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange.
`"rateLimit": 3100` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
!!! Note
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
## Binance
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).

View File

@ -54,9 +54,11 @@ you can't say much from few trades.
Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
### I want to improve the bot with a new strategy
### I want to use incomplete candles
That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened.
You can use "current" market data by using the [dataprovider](strategy-customization.md#orderbookpair-maximum)'s orderbook or ticker methods - which however cannot be used during backtesting.
### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
@ -82,11 +84,11 @@ Currently known to happen for US Bittrex users.
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex).
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io).
To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market":
To fix this, redefine order types in the strategy to use "limit" instead of "market":
```
order_types = {
@ -136,6 +138,8 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us
> type \path\to\mylogfile.log | findstr "something"
```
## Hyperopt module
### Why does freqtrade not have GPU support?
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
@ -152,8 +156,6 @@ The benefit of using GPU would therefore be pretty slim - and will not justify t
There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity).
## Hyperopt module
### How many epochs do I need to get a good Hyperopt result?
Per default Hyperopt called without the `-e`/`--epochs` command line option will only

View File

@ -60,7 +60,7 @@ optional arguments:
Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `None`).
(default: `json`).
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
@ -114,7 +114,8 @@ optional arguments:
Hyperopt-loss-functions are:
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
SortinoHyperOptLoss, SortinoHyperOptLossDaily
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
MaxDrawDownHyperOptLoss
--disable-param-export
Disable automatic hyperopt parameter export.
@ -512,12 +513,13 @@ This class should be in its own file within the `user_data/hyperopts/` directory
Currently, the following loss functions are builtin:
* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation)
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation)
* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation)
* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation)
* `ShortTradeDurHyperOptLoss` - (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
* `OnlyProfitHyperOptLoss` - takes only amount of profit into consideration.
* `SharpeHyperOptLoss` - optimizes Sharpe Ratio calculated on trade returns relative to standard deviation.
* `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation.
* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.

View File

@ -113,6 +113,13 @@ git checkout develop
You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands.
??? Note "Install from pypi"
An alternative way to install Freqtrade is from [pypi](https://pypi.org/project/freqtrade/). The downside is that this method requires ta-lib to be correctly installed beforehand, and is therefore currently not the recommended way to install Freqtrade.
``` bash
pip install freqtrade
```
------
## Script Installation

View File

@ -1,4 +1,4 @@
mkdocs==1.2.2
mkdocs-material==7.3.0
mkdocs-material==7.3.2
mdx_truly_sane_lists==1.2
pymdown-extensions==8.2
pymdown-extensions==9.0

View File

@ -78,7 +78,7 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
},
```
Uncomment the following from your docker-compose file:
Make sure that the following 2 lines are available in your docker-compose file:
```yml
ports:

View File

@ -29,7 +29,7 @@ dependencies:
- colorama
- questionary
- prompt-toolkit
- schedule
# ============================
# 2/4 req dev

View File

@ -73,7 +73,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "timeframe", "plot_auto_open"]
ARGS_INSTALL_UI = ["erase_ui_only"]
ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version']
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]

View File

@ -163,7 +163,8 @@ def ask_user_config() -> Dict[str, Any]:
{
"type": "text",
"name": "api_server_listen_addr",
"message": "Insert Api server Listen Address (best left untouched default!)",
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
"otherwise best left untouched)"),
"default": "127.0.0.1",
"when": lambda x: x['api_server']
},

View File

@ -414,6 +414,12 @@ AVAILABLE_CLI_OPTIONS = {
action='store_true',
default=False,
),
"ui_version": Arg(
'--ui-version',
help=('Specify a specific version of FreqUI to install. '
'Not specifying this installs the latest version.'),
type=str,
),
# Templating options
"template": Arg(
'--template',

View File

@ -128,7 +128,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
f.write(version)
def get_ui_download_url() -> Tuple[str, str]:
def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
base_url = 'https://api.github.com/repos/freqtrade/frequi/'
# Get base UI Repo path
@ -136,6 +136,14 @@ def get_ui_download_url() -> Tuple[str, str]:
resp.raise_for_status()
r = resp.json()
if version:
tmp = [x for x in r if x['name'] == version]
if tmp:
latest_version = tmp[0]['name']
assets = tmp[0].get('assets', [])
else:
raise ValueError("UI-Version not found.")
else:
latest_version = r[0]['name']
assets = r[0].get('assets', [])
dl_url = ''
@ -156,7 +164,7 @@ def start_install_ui(args: Dict[str, Any]) -> None:
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
# First make sure the assets are removed.
dl_url, latest_version = get_ui_download_url()
dl_url, latest_version = get_ui_download_url(args.get('ui_version'))
curr_version = read_ui_version(dest_folder)
if curr_version == latest_version and not args.get('erase_ui_only'):

View File

@ -24,7 +24,8 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
'MaxDrawDownHyperOptLoss']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',

View File

@ -1,6 +1,6 @@
""" Bibox exchange subclass """
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exchange import Exchange
@ -24,3 +24,5 @@ class Bibox(Exchange):
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
return {"has": {"fetchCurrencies": False}}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@ -28,6 +28,8 @@ class Binance(Exchange):
"trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
# but the schedule won't check within this timeframe
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list

View File

@ -1,7 +1,8 @@
""" Bybit exchange subclass """
import logging
from typing import Dict
from typing import Dict, List, Tuple
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exchange import Exchange
@ -21,3 +22,11 @@ class Bybit(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 200,
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
]

View File

@ -9,7 +9,7 @@ import logging
from copy import deepcopy
from datetime import datetime, timezone
from math import ceil
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
import ccxt
@ -72,6 +72,10 @@ class Exchange:
}
_ft_has: Dict = {}
# funding_fee_times is currently unused, but should ideally be used to properly
# schedule refresh times
funding_fee_times: List[int] = [] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
]
@ -503,7 +507,7 @@ class Exchange:
if startup_candles + 5 > candle_limit:
raise OperationalException(
f"This strategy requires {startup_candles} candles to start. "
f"{self.name} only provides {candle_limit} for {timeframe}.")
f"{self.name} only provides {candle_limit - 5} for {timeframe}.")
def validate_trading_mode_and_collateral(
self,
@ -565,7 +569,7 @@ class Exchange:
precision = self.markets[pair]['precision']['price']
missing = price % precision
if missing != 0:
price = price - missing + precision
price = round(price - missing + precision, 10)
else:
symbol_prec = self.markets[pair]['precision']['price']
big_price = price * pow(10, symbol_prec)
@ -1130,7 +1134,7 @@ class Exchange:
ticker_rate = ticker[conf_strategy['price_side']]
if ticker['last'] and ticker_rate:
if side == 'buy' and ticker_rate > ticker['last']:
balance = conf_strategy['ask_last_balance']
balance = conf_strategy.get('ask_last_balance', 0.0)
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
elif side == 'sell' and ticker_rate < ticker['last']:
balance = conf_strategy.get('bid_last_balance', 0.0)
@ -1600,6 +1604,37 @@ class Exchange:
self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id))
@retrier
def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
"""
Returns the sum of all funding fees that were exchanged for a pair within a timeframe
:param pair: (e.g. ADA/USDT)
:param since: The earliest time of consideration for calculating funding fees,
in unix time or as a datetime
"""
# TODO-lev: Add dry-run handling for this.
if not self.exchange_has("fetchFundingHistory"):
raise OperationalException(
f"fetch_funding_history() has not been implemented on ccxt.{self.name}")
if type(since) is datetime:
since = int(since.timestamp()) * 1000 # * 1000 for ms
try:
funding_history = self._api.fetch_funding_history(
pair=pair,
since=since
)
return sum(fee['amount'] for fee in funding_history)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def fill_leverage_brackets(self):
"""
# TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken

View File

@ -21,6 +21,7 @@ class Ftx(Exchange):
"stoploss_on_exchange": True,
"ohlcv_candle_limit": 1500,
}
funding_fee_times: List[int] = list(range(0, 24))
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list

View File

@ -1,7 +1,8 @@
""" Gate.io exchange subclass """
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
@ -23,3 +24,12 @@ class Gateio(Exchange):
}
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')

View File

@ -1,5 +1,5 @@
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exchange import Exchange
@ -21,3 +21,5 @@ class Hitbtc(Exchange):
"ohlcv_candle_limit": 1000,
"ohlcv_params": {"sort": "DESC"}
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@ -23,6 +23,7 @@ class Kraken(Exchange):
"trades_pagination": "id",
"trades_pagination_arg": "since",
}
funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list

View File

@ -1,6 +1,6 @@
""" Kucoin exchange subclass """
import logging
from typing import Dict
from typing import Dict, List
from freqtrade.exchange import Exchange
@ -24,3 +24,5 @@ class Kucoin(Exchange):
"order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce",
}
funding_fee_times: List[int] = [4, 12, 20] # hours of the day

View File

@ -4,12 +4,13 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
import copy
import logging
import traceback
from datetime import datetime, timezone
from datetime import datetime, time, timezone
from math import isclose
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple
import arrow
from schedule import Scheduler
from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency
@ -108,14 +109,26 @@ class FreqtradeBot(LoggingMixin):
self.trading_mode: TradingMode = TradingMode.SPOT
self.collateral_type: Optional[Collateral] = None
trading_mode = self.config.get('trading_mode')
collateral_type = self.config.get('collateral_type')
if 'trading_mode' in self.config:
self.trading_mode = TradingMode(self.config['trading_mode'])
if trading_mode:
self.trading_mode = TradingMode(trading_mode)
if 'collateral_type' in self.config:
self.collateral_type = Collateral(self.config['collateral_type'])
if collateral_type:
self.collateral_type = Collateral(collateral_type)
self._schedule = Scheduler()
if self.trading_mode == TradingMode.FUTURES:
def update():
self.update_funding_fees()
self.wallets.update()
# TODO: This would be more efficient if scheduled in utc time, and performed at each
# TODO: funding interval, specified by funding_fee_times on the exchange classes
for time_slot in range(0, 24):
for minutes in [0, 15, 30, 45]:
t = str(time(time_slot, minutes, 2))
self._schedule.every().day.at(t).do(update)
def notify_status(self, msg: str) -> None:
"""
@ -196,7 +209,8 @@ class FreqtradeBot(LoggingMixin):
# Then looking for buy opportunities
if self.get_free_open_trades():
self.enter_positions()
if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending()
Trade.commit()
def process_stopped(self) -> None:
@ -252,6 +266,15 @@ class FreqtradeBot(LoggingMixin):
open_trades = len(Trade.get_open_trades())
return max(0, self.config['max_open_trades'] - open_trades)
def update_funding_fees(self):
if self.trading_mode == TradingMode.FUTURES:
for trade in Trade.get_open_trades():
funding_fees = self.exchange.get_funding_fees_from_exchange(
trade.pair,
trade.open_date
)
trade.funding_fees = funding_fees
def startup_update_open_orders(self):
"""
Updates open orders based on order list kept in the database.
@ -274,6 +297,9 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Error updating Order {order.order_id} due to {e}")
if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending()
def update_closed_trades_without_assigned_fees(self):
"""
Update closed trades without close fees assigned.
@ -679,6 +705,12 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
open_date = datetime.now(timezone.utc)
if self.trading_mode == TradingMode.FUTURES:
funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date)
else:
funding_fees = 0.0
trade = Trade(
pair=pair,
stake_amount=stake_amount,
@ -689,7 +721,7 @@ class FreqtradeBot(LoggingMixin):
fee_close=fee,
open_rate=enter_limit_filled_price,
open_rate_requested=enter_limit_requested,
open_date=datetime.utcnow(),
open_date=open_date,
exchange=self.exchange.id,
open_order_id=order_id,
strategy=self.strategy.get_strategy_name(),
@ -700,6 +732,8 @@ class FreqtradeBot(LoggingMixin):
is_short=is_short,
interest_rate=interest_rate,
isolated_liq=isolated_liq,
trading_mode=self.trading_mode,
funding_fees=funding_fees
)
trade.orders.append(order_obj)

View File

@ -0,0 +1,41 @@
"""
MaxDrawDownHyperOptLoss
This module defines the alternative HyperOptLoss class which can be used for
Hyperoptimization.
"""
from datetime import datetime
from pandas import DataFrame
from freqtrade.data.btanalysis import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
class MaxDrawDownHyperOptLoss(IHyperOptLoss):
"""
Defines the loss function for hyperopt.
This implementation optimizes for max draw down and profit
Less max drawdown more profit -> Lower return value
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
"""
Objective function.
Uses profit ratio weighted max_drawdown when drawdown is available.
Otherwise directly optimizes profit ratio.
"""
total_profit = results['profit_abs'].sum()
try:
max_drawdown = calculate_max_drawdown(results, value_col='profit_abs')
except ValueError:
# No losing trade, therefore no drawdown.
return -total_profit
return -total_profit / max_drawdown[0]

View File

@ -49,11 +49,20 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
strategy = get_column_def(cols, 'strategy', 'null')
buy_tag = get_column_def(cols, 'buy_tag', 'null')
trading_mode = get_column_def(cols, 'trading_mode', 'null')
# Leverage Properties
leverage = get_column_def(cols, 'leverage', '1.0')
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
isolated_liq = get_column_def(cols, 'isolated_liq', 'null')
# sqlite does not support literals for booleans
is_short = get_column_def(cols, 'is_short', '0')
# Margin Properties
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
# Futures properties
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
@ -91,7 +100,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
timeframe, open_trade_value, close_profit_abs,
leverage, interest_rate, isolated_liq, is_short
trading_mode, leverage, isolated_liq, is_short,
interest_rate, funding_fees
)
select id, lower(exchange), pair,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
@ -108,8 +118,9 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{sell_order_status} sell_order_status,
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{leverage} leverage, {interest_rate} interest_rate,
{isolated_liq} isolated_liq, {is_short} is_short
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
{is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees
from {table_back_name}
"""))
@ -169,7 +180,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
table_back_name = get_backup_name(tabs, 'trades_bak')
# Check for latest column
if not has_column(cols, 'is_short'):
if not has_column(cols, 'funding_fees'):
logger.info(f'Running database migration for trades - backup: {table_back_name}')
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
# Reread columns - the above recreated the table!

View File

@ -6,7 +6,7 @@ from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType
from freqtrade.enums import SellType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest
from freqtrade.misc import safe_value_fallback
@ -265,14 +265,19 @@ class LocalTrade():
buy_tag: Optional[str] = None
timeframe: Optional[int] = None
trading_mode: TradingMode = TradingMode.SPOT
# Leverage trading properties
is_short: bool = False
isolated_liq: Optional[float] = None
is_short: bool = False
leverage: float = 1.0
# Margin trading properties
interest_rate: float = 0.0
# Futures properties
funding_fees: Optional[float] = None
@property
def has_no_leverage(self) -> bool:
"""Returns true if this is a non-leverage, non-short trade"""
@ -439,7 +444,8 @@ class LocalTrade():
'interest_rate': self.interest_rate,
'isolated_liq': self.isolated_liq,
'is_short': self.is_short,
'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id,
}
@ -642,7 +648,7 @@ class LocalTrade():
zero = Decimal(0.0)
# If nothing was borrowed
if self.has_no_leverage:
if self.has_no_leverage or self.trading_mode != TradingMode.MARGIN:
return zero
open_date = self.open_date.replace(tzinfo=None)
@ -656,6 +662,17 @@ class LocalTrade():
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
fee: Optional[float] = None) -> Decimal:
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
if self.is_short:
return close_trade + fees
else:
return close_trade - fees
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
@ -672,20 +689,32 @@ class LocalTrade():
if rate is None and not self.close_rate:
return 0.0
interest = self.calculate_interest(interest_rate)
amount = Decimal(self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, fee))
elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest(interest_rate)
if self.is_short:
amount = Decimal(self.amount) + Decimal(interest)
amount = amount + total_interest
return float(self._calc_base_close(amount, rate, fee))
else:
# Currency already owned for longs, no need to purchase
amount = Decimal(self.amount)
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
return float(self._calc_base_close(amount, rate, fee) - total_interest)
elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0
if self.is_short:
return float(close_trade + fees)
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
else:
return float(close_trade - fees - interest)
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
else:
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None,
@ -893,6 +922,8 @@ class Trade(_DECL_BASE, LocalTrade):
buy_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True)
# Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False)
@ -901,6 +932,9 @@ class Trade(_DECL_BASE, LocalTrade):
# Margin Trading Properties
interest_rate = Column(Float, nullable=False, default=0.0)
# Futures properties
funding_fees = Column(Float, nullable=True, default=None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.recalc_open_trade_value()

View File

@ -347,3 +347,8 @@ class BacktestResponse(BaseModel):
trade_count: Optional[float]
# TODO: Properly type backtestresult...
backtest_result: Optional[Dict[str, Any]]
class SysInfo(BaseModel):
cpu_pct: List[float]
ram_pct: float

View File

@ -18,7 +18,8 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
OpenTradeSchema, PairHistory, PerformanceEntry,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
Stats, StatusMsg, StrategyListResponse,
StrategyResponse, Version, WhitelistResponse)
StrategyResponse, SysInfo, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
@ -259,3 +260,8 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
'pair_interval': pair_interval,
}
return result
@router.get('/sysinfo', response_model=SysInfo, tags=['info'])
def sysinfo():
return RPC._rpc_sysinfo()

View File

@ -8,6 +8,7 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
import psutil
from numpy import NAN, inf, int64, mean
from pandas import DataFrame
@ -871,3 +872,10 @@ class RPC:
'subplots' not in self._freqtrade.strategy.plot_config):
self._freqtrade.strategy.plot_config['subplots'] = {}
return self._freqtrade.strategy.plot_config
@staticmethod
def _rpc_sysinfo() -> Dict[str, Any]:
return {
"cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
"ram_pct": psutil.virtual_memory().percent
}

View File

@ -2,11 +2,8 @@
"name": "{{ exchange_name | lower }}",
"key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
},
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
],
"pair_blacklist": [

View File

@ -2,10 +2,8 @@
"name": "{{ exchange_name | lower }}",
"key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": true
},
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
],

View File

@ -3,14 +3,8 @@
"key": "{{ exchange_key }}",
"secret": "{{ exchange_secret }}",
"password": "{{ exchange_key_password }}",
"ccxt_config": {
"enableRateLimit": true,
"rateLimit": 200
},
"ccxt_async_config": {
"enableRateLimit": true,
"rateLimit": 200
},
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
],
"pair_blacklist": [

View File

@ -32,8 +32,7 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, dataframe: DataFrame,
**kwargs) -> float:
current_rate: float, current_profit: float, **kwargs) -> float:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate.
@ -44,14 +43,13 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
When not implemented by a strategy, returns the initial stoploss value
Only called when use_custom_stoploss is set to True.
:param pair: Pair that's about to be sold.
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the currentrate
:return float: New stoploss value, relative to the current_rate
"""
return self.stoploss

View File

@ -4,13 +4,12 @@
-r requirements-hyperopt.txt
coveralls==3.2.0
flake8==3.9.2
flake8-type-annotations==0.1.0
flake8-tidy-imports==4.4.1
flake8==4.0.0
flake8-tidy-imports==4.5.0
mypy==0.910
pytest==6.2.5
pytest-asyncio==0.15.1
pytest-cov==2.12.1
pytest-cov==3.0.0
pytest-mock==3.6.1
pytest-random-order==1.0.4
isort==5.9.3
@ -21,7 +20,7 @@ time-machine==2.4.0
nbconvert==6.2.0
# mypy types
types-cachetools==4.2.0
types-filelock==0.1.5
types-cachetools==4.2.2
types-filelock==3.2.0
types-requests==2.25.9
types-tabulate==0.8.2

View File

@ -3,9 +3,9 @@
# Required for hyperopt
scipy==1.7.1
scikit-learn==0.24.2
scikit-optimize==0.8.1
filelock==3.0.12
joblib==1.0.1
scikit-learn==1.0
scikit-optimize==0.9.0
filelock==3.3.0
joblib==1.1.0
psutil==5.8.0
progressbar2==3.53.3

View File

@ -2,25 +2,25 @@ numpy==1.21.2
pandas==1.3.3
pandas-ta==0.3.14b
ccxt==1.57.3
ccxt==1.57.94
# Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.8
cryptography==35.0.0
aiohttp==3.7.4.post0
SQLAlchemy==1.4.25
python-telegram-bot==13.7
arrow==1.1.1
arrow==1.2.0
cachetools==4.2.2
requests==2.26.0
urllib3==1.26.7
wrapt==1.12.1
jsonschema==3.2.0
wrapt==1.13.1
jsonschema==4.1.0
TA-Lib==0.4.21
technical==1.3.0
tabulate==0.8.9
pycoingecko==2.2.0
jinja2==3.0.1
jinja2==3.0.2
tables==3.6.1
blosc==1.10.4
blosc==1.10.6
# find first, C search in arrays
py_find_1st==1.1.5
@ -34,11 +34,15 @@ sdnotify==0.3.2
# API Server
fastapi==0.68.1
uvicorn==0.15.0
pyjwt==2.1.0
pyjwt==2.2.0
aiofiles==0.7.0
psutil==5.8.0
# Support for colorized terminal output
colorama==0.4.4
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.20
#Futures
schedule==1.1.0

View File

@ -334,6 +334,13 @@ class FtRestClient():
"timerange": timerange if timerange else '',
})
def sysinfo(self):
"""Provides system information (CPU, RAM usage)
:return: json object
"""
return self._get("sysinfo")
def add_arguments():
parser = argparse.ArgumentParser()

View File

@ -72,7 +72,8 @@ setup(
'fastapi',
'uvicorn',
'pyjwt',
'aiofiles'
'aiofiles',
'schedule'
],
extras_require={
'dev': all_extra,

View File

@ -605,16 +605,33 @@ def test_get_ui_download_url(mocker):
def test_get_ui_download_url_direct(mocker):
response = MagicMock()
response.json = MagicMock(
side_effect=[[{
return_value=[
{
'assets_url': 'http://whatever.json',
'name': '0.0.2',
'assets': [{'browser_download_url': 'http://download22.zip'}]
},
{
'assets_url': 'http://whatever.json',
'name': '0.0.1',
'assets': [{'browser_download_url': 'http://download11.zip'}]}]])
'assets': [{'browser_download_url': 'http://download1.zip'}]
},
])
get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get",
return_value=response)
x, last_version = get_ui_download_url()
assert get_mock.call_count == 1
assert last_version == '0.0.2'
assert x == 'http://download22.zip'
get_mock.reset_mock()
response.json.reset_mock()
x, last_version = get_ui_download_url('0.0.1')
assert last_version == '0.0.1'
assert x == 'http://download11.zip'
assert x == 'http://download1.zip'
with pytest.raises(ValueError, match="UI-Version not found."):
x, last_version = get_ui_download_url('0.0.3')
def test_download_data_keyboardInterrupt(mocker, caplog, markets):

View File

@ -277,6 +277,7 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci
(234.43, 4, 0.5, 234.5),
(234.53, 4, 0.5, 235.0),
(0.891534, 4, 0.0001, 0.8916),
(64968.89, 4, 0.01, 64968.89),
])
def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
@ -295,7 +296,7 @@ def test_price_to_precision(default_conf, mocker, price, precision_mode, precisi
PropertyMock(return_value=precision_mode))
pair = 'ETH/BTC'
assert pytest.approx(exchange.price_to_precision(pair, price)) == expected
assert exchange.price_to_precision(pair, price) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
@ -1895,6 +1896,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
('ask', 20, 19, 10, 0.3, 17), # Between ask and last
('ask', 5, 6, 10, 1.0, 5), # last bigger than ask
('ask', 5, 6, 10, 0.5, 5), # last bigger than ask
('ask', 20, 19, 10, None, 20), # ask_last_balance missing
('ask', 10, 20, None, 0.5, 10), # last not available - uses ask
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
('ask', 4, 5, None, 1, 4), # last not available - uses ask
@ -1905,6 +1907,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
('bid', 21, 20, 10, 0.7, 13), # Between bid and last
('bid', 21, 20, 10, 0.3, 17), # Between bid and last
('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
('bid', 21, 20, 10, None, 20), # ask_last_balance missing
('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
@ -1914,6 +1917,9 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG)
if last_ab is None:
del default_conf['bid_strategy']['ask_last_balance']
else:
default_conf['bid_strategy']['ask_last_balance'] = last_ab
default_conf['bid_strategy']['price_side'] = side
exchange = get_patched_exchange(mocker, default_conf)
@ -1939,6 +1945,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
('bid', 0.003, 0.002, 0.005, None, 0.002),
('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
@ -1949,12 +1956,14 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
('ask', 0.006, 1.0, 11.0, None, 0.006),
])
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask,
last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG)
default_conf['ask_strategy']['price_side'] = side
if last_ab is not None:
default_conf['ask_strategy']['bid_last_balance'] = last_ab
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': ask, 'bid': bid, 'last': last})
@ -3048,6 +3057,74 @@ def test_calculate_backoff(retrycount, max_retries, expected):
assert calculate_backoff(retrycount, max_retries) == expected
@pytest.mark.parametrize("exchange_name", ['binance', 'ftx'])
def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
api_mock = MagicMock()
api_mock.fetch_funding_history = MagicMock(return_value=[
{
'amount': 0.14542,
'code': 'USDT',
'datetime': '2021-09-01T08:00:01.000Z',
'id': '485478',
'info': {'asset': 'USDT',
'income': '0.14542',
'incomeType': 'FUNDING_FEE',
'info': 'FUNDING_FEE',
'symbol': 'XRPUSDT',
'time': '1630382001000',
'tradeId': '',
'tranId': '993203'},
'symbol': 'XRP/USDT',
'timestamp': 1630382001000
},
{
'amount': -0.14642,
'code': 'USDT',
'datetime': '2021-09-01T16:00:01.000Z',
'id': '485479',
'info': {'asset': 'USDT',
'income': '-0.14642',
'incomeType': 'FUNDING_FEE',
'info': 'FUNDING_FEE',
'symbol': 'XRPUSDT',
'time': '1630314001000',
'tradeId': '',
'tranId': '993204'},
'symbol': 'XRP/USDT',
'timestamp': 1630314001000
}
])
type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True})
# mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ')
unix_time = int(date_time.timestamp())
expected_fees = -0.001 # 0.14542341 + -0.14642341
fees_from_datetime = exchange.get_funding_fees_from_exchange(
pair='XRP/USDT',
since=date_time
)
fees_from_unix_time = exchange.get_funding_fees_from_exchange(
pair='XRP/USDT',
since=unix_time
)
assert(isclose(expected_fees, fees_from_datetime))
assert(isclose(expected_fees, fees_from_unix_time))
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
exchange_name,
"get_funding_fees_from_exchange",
"fetch_funding_history",
pair="XRP/USDT",
since=unix_time
)
@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx'])
@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [
(9.0, 3.0, 3.0),

View File

@ -0,0 +1,28 @@
import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Gateio
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
def test_validate_order_types_gateio(default_conf, mocker):
default_conf['exchange']['name'] = 'gateio'
mocker.patch('freqtrade.exchange.Exchange._init_ccxt')
mocker.patch('freqtrade.exchange.Exchange._load_markets', return_value={})
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
exch = ExchangeResolver.load_exchange('gateio', default_conf, True)
assert isinstance(exch, Gateio)
default_conf['order_types'] = {
'buy': 'market',
'sell': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gateio', default_conf, True)

View File

@ -84,13 +84,14 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
"SortinoHyperOptLossDaily",
"SharpeHyperOptLoss",
"SharpeHyperOptLossDaily",
"MaxDrawDownHyperOptLoss",
])
def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None:
results_over = hyperopt_results.copy()
results_over['profit_abs'] = hyperopt_results['profit_abs'] * 2
results_over['profit_abs'] = hyperopt_results['profit_abs'] * 2 + 0.2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2
results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2 - 0.2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': lossfunction})

View File

@ -8,7 +8,7 @@ import pytest
from numpy import isnan
from freqtrade.edge import PairInfo
from freqtrade.enums import State
from freqtrade.enums import State, TradingMode
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade
from freqtrade.persistence.pairlock_middleware import PairLocks
@ -112,6 +112,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'interest_rate': 0.0,
'isolated_liq': None,
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT
}
mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -183,6 +185,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'interest_rate': 0.0,
'isolated_liq': None,
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT
}

View File

@ -1282,6 +1282,16 @@ def test_list_available_pairs(botclient):
assert len(rc.json()['pair_interval']) == 1
def test_sysinfo(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/sysinfo")
assert_response(rc)
result = rc.json()
assert 'cpu_pct' in result
assert 'ram_pct' in result
def test_api_backtesting(botclient, mocker, fee, caplog):
ftbot, client = botclient
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)

View File

@ -4,14 +4,14 @@
import logging
import time
from copy import deepcopy
from math import floor, isclose
from math import isclose
from unittest.mock import ANY, MagicMock, PropertyMock
import arrow
import pytest
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State
from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError,
TemporaryError)
@ -2832,16 +2832,22 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
)
# Prevented sell ...
# TODO-lev: side="buy"
freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI))
freqtrade.execute_trade_exit(
trade=trade,
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI)
)
assert rpc_mock.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
# Repatch with true
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
# TODO-lev: side="buy"
freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI))
freqtrade.execute_trade_exit(
trade=trade,
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI)
)
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
assert rpc_mock.call_count == 1
@ -4627,3 +4633,36 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None:
def test_leverage_prep():
# TODO-lev
return
@pytest.mark.parametrize('trading_mode,calls,t1,t2', [
(TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
(TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
(TradingMode.FUTURES, 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"),
(TradingMode.FUTURES, 32, "2021-09-01 00:00:00", "2021-09-01 08:00:01"),
(TradingMode.FUTURES, 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"),
(TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
(TradingMode.FUTURES, 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
])
def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine,
t1, t2):
time_machine.move_to(f"{t1} +00:00")
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_funding_fees', return_value=True)
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = 'isolated'
freqtrade = get_patched_freqtradebot(mocker, default_conf)
time_machine.move_to(f"{t2} +00:00")
# Check schedule jobs in debugging with freqtrade._schedule.jobs
freqtrade._schedule.run_pending()
assert freqtrade.update_funding_fees.call_count == calls

View File

@ -11,12 +11,16 @@ import pytest
from sqlalchemy import create_engine, inspect, text
from freqtrade import constants
from freqtrade.enums import TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
from tests.conftest import (create_mock_trades, create_mock_trades_with_leverage, get_sides,
log_has, log_has_re)
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'], default_conf['dry_run'])
@ -81,7 +85,8 @@ def test_enter_exit_side(fee, is_short):
fee_close=fee.return_value,
exchange='binance',
is_short=is_short,
leverage=2.0
leverage=2.0,
trading_mode=margin
)
assert trade.enter_side == enter_side
assert trade.exit_side == exit_side
@ -101,7 +106,8 @@ def test_set_stop_loss_isolated_liq(fee):
fee_close=fee.return_value,
exchange='binance',
is_short=False,
leverage=2.0
leverage=2.0,
trading_mode=margin
)
trade.set_isolated_liq(0.09)
assert trade.isolated_liq == 0.09
@ -168,32 +174,40 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.initial_stop_loss == 0.09
@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest', [
("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8)),
("binance", True, 3, 10, 0.0005, 0.000625),
("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8)),
("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8)),
("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8)),
("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8)),
("binance", False, 5, 295, 0.0005, 0.005),
("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8)),
("binance", False, 1, 295, 0.0005, 0.0),
("binance", True, 1, 295, 0.0005, 0.003125),
@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [
("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8), margin),
("binance", True, 3, 10, 0.0005, 0.000625, margin),
("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8), margin),
("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8), margin),
("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8), margin),
("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8), margin),
("binance", False, 5, 295, 0.0005, 0.005, margin),
("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8), margin),
("binance", False, 1, 295, 0.0005, 0.0, spot),
("binance", True, 1, 295, 0.0005, 0.003125, margin),
("kraken", False, 3, 10, 0.0005, 0.040),
("kraken", True, 3, 10, 0.0005, 0.030),
("kraken", False, 3, 295, 0.0005, 0.06),
("kraken", True, 3, 295, 0.0005, 0.045),
("kraken", False, 3, 295, 0.00025, 0.03),
("kraken", True, 3, 295, 0.00025, 0.0225),
("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8)),
("kraken", True, 5, 295, 0.0005, 0.045),
("kraken", False, 1, 295, 0.0005, 0.0),
("kraken", True, 1, 295, 0.0005, 0.045),
("binance", False, 3, 10, 0.0005, 0.0, futures),
("binance", True, 3, 295, 0.0005, 0.0, futures),
("binance", False, 5, 295, 0.0005, 0.0, futures),
("binance", True, 5, 295, 0.0005, 0.0, futures),
("binance", False, 1, 295, 0.0005, 0.0, futures),
("binance", True, 1, 295, 0.0005, 0.0, futures),
("kraken", False, 3, 10, 0.0005, 0.040, margin),
("kraken", True, 3, 10, 0.0005, 0.030, margin),
("kraken", False, 3, 295, 0.0005, 0.06, margin),
("kraken", True, 3, 295, 0.0005, 0.045, margin),
("kraken", False, 3, 295, 0.00025, 0.03, margin),
("kraken", True, 3, 295, 0.00025, 0.0225, margin),
("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8), margin),
("kraken", True, 5, 295, 0.0005, 0.045, margin),
("kraken", False, 1, 295, 0.0005, 0.0, spot),
("kraken", True, 1, 295, 0.0005, 0.045, margin),
])
@pytest.mark.usefixtures("init_persistence")
def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, rate, interest):
def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, rate, interest,
trading_mode):
"""
10min, 5hr limit trade on Binance/Kraken at 3x,5x leverage
fee: 0.25 % quote
@ -258,21 +272,22 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes,
exchange=exchange,
leverage=lev,
interest_rate=rate,
is_short=is_short
is_short=is_short,
trading_mode=trading_mode
)
assert round(float(trade.calculate_interest()), 8) == interest
@pytest.mark.parametrize('is_short,lev,borrowed', [
(False, 1.0, 0.0),
(True, 1.0, 30.0),
(False, 3.0, 40.0),
(True, 3.0, 30.0),
@pytest.mark.parametrize('is_short,lev,borrowed,trading_mode', [
(False, 1.0, 0.0, spot),
(True, 1.0, 30.0, margin),
(False, 3.0, 40.0, margin),
(True, 3.0, 30.0, margin),
])
@pytest.mark.usefixtures("init_persistence")
def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee,
caplog, is_short, lev, borrowed):
caplog, is_short, lev, borrowed, trading_mode):
"""
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote
@ -347,18 +362,19 @@ def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee,
fee_close=fee.return_value,
exchange='binance',
is_short=is_short,
leverage=lev
leverage=lev,
trading_mode=trading_mode
)
assert trade.borrowed == borrowed
@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit', [
(False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8)),
(True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8))
@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit,trading_mode', [
(False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8), spot),
(True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8), margin),
])
@pytest.mark.usefixtures("init_persistence")
def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_usdt,
is_short, open_rate, close_rate, lev, profit):
is_short, open_rate, close_rate, lev, profit, trading_mode):
"""
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote
@ -445,7 +461,8 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
exchange='binance',
is_short=is_short,
interest_rate=0.0005,
leverage=lev
leverage=lev,
trading_mode=trading_mode
)
assert trade.open_order_id is None
assert trade.close_profit is None
@ -491,6 +508,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
fee_close=fee.return_value,
open_date=arrow.utcnow().datetime,
exchange='binance',
trading_mode=margin
)
trade.open_order_id = 'something'
@ -518,20 +536,28 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
caplog)
@pytest.mark.parametrize('exchange,is_short,lev,open_value,close_value,profit,profit_ratio', [
("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232),
("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.1055368159983292),
("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534),
("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876),
@pytest.mark.parametrize(
'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode,funding_fees', [
("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0),
("binance", True, 1, 59.850, 66.1663784375, -6.3163784375, -0.105536815998329, margin, 0.0),
("binance", False, 3, 60.15, 65.83416667, 5.68416667, 0.2834995845386534, margin, 0.0),
("binance", True, 3, 59.85, 66.1663784375, -6.3163784375, -0.3166104479949876, margin, 0.0),
("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232),
("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614),
("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419),
("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842),
("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0),
("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614, margin, 0.0),
("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419, margin, 0.0),
("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, margin, 0.0),
("binance", False, 1, 60.15, 66.835, 6.685, 0.11113881961762262, futures, 1.0),
("binance", True, 1, 59.85, 67.165, -7.315, -0.12222222222222223, futures, -1.0),
("binance", False, 3, 60.15, 64.835, 4.685, 0.23366583541147135, futures, -1.0),
("binance", True, 3, 59.85, 65.165, -5.315, -0.26641604010025066, futures, 1.0),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange,
is_short, lev, open_value, close_value, profit, profit_ratio):
def test_calc_open_close_trade_price(
limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, is_short, lev,
open_value, close_value, profit, profit_ratio, trading_mode, funding_fees
):
trade: Trade = Trade(
pair='ADA/USDT',
stake_amount=60.0,
@ -543,7 +569,9 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt
fee_close=fee.return_value,
exchange=exchange,
is_short=is_short,
leverage=lev
leverage=lev,
trading_mode=trading_mode,
funding_fees=funding_fees
)
trade.open_order_id = f'something-{is_short}-{lev}-{exchange}'
@ -572,6 +600,7 @@ def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee):
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10),
interest_rate=0.0005,
exchange='binance',
trading_mode=margin
)
assert trade.close_profit is None
assert trade.close_date is None
@ -600,6 +629,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='binance',
trading_mode=margin
)
trade.open_order_id = 'something'
@ -617,6 +647,7 @@ def test_update_open_order(limit_buy_order_usdt):
fee_open=0.1,
fee_close=0.1,
exchange='binance',
trading_mode=margin
)
assert trade.open_order_id is None
@ -641,6 +672,7 @@ def test_update_invalid_order(limit_buy_order_usdt):
fee_open=0.1,
fee_close=0.1,
exchange='binance',
trading_mode=margin
)
limit_buy_order_usdt['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
@ -648,6 +680,7 @@ def test_update_invalid_order(limit_buy_order_usdt):
@pytest.mark.parametrize('exchange', ['binance', 'kraken'])
@pytest.mark.parametrize('trading_mode', [spot, margin, futures])
@pytest.mark.parametrize('lev', [1, 3])
@pytest.mark.parametrize('is_short,fee_rate,result', [
(False, 0.003, 60.18),
@ -666,7 +699,8 @@ def test_calc_open_trade_value(
lev,
is_short,
fee_rate,
result
result,
trading_mode
):
# 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
# fee: 0.25 %, 0.3% quote
@ -692,7 +726,8 @@ def test_calc_open_trade_value(
fee_close=fee_rate,
exchange=exchange,
leverage=lev,
is_short=is_short
is_short=is_short,
trading_mode=trading_mode
)
trade.open_order_id = 'open_trade'
@ -700,26 +735,37 @@ def test_calc_open_trade_value(
assert trade._calc_open_trade_value() == result
@pytest.mark.parametrize('exchange,is_short,lev,open_rate,close_rate,fee_rate,result', [
('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125),
('binance', False, 1, 2.0, 2.5, 0.003, 74.775),
('binance', False, 1, 2.0, 2.2, 0.005, 65.67),
('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667),
('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667),
('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725),
('kraken', False, 3, 2.0, 2.5, 0.003, 74.735),
('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875),
('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225),
('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641),
('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719),
('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641),
('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719),
('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875),
('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225),
@pytest.mark.parametrize(
'exchange,is_short,lev,open_rate,close_rate,fee_rate,result,trading_mode,funding_fees', [
('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125, spot, 0),
('binance', False, 1, 2.0, 2.5, 0.003, 74.775, spot, 0),
('binance', False, 1, 2.0, 2.2, 0.005, 65.67, margin, 0),
('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667, margin, 0),
('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, margin, 0),
('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641, margin, 0),
('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719, margin, 0),
('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641, margin, 0),
('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, margin, 0),
# Kraken
('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725, margin, 0),
('kraken', False, 3, 2.0, 2.5, 0.003, 74.735, margin, 0),
('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875, margin, 0),
('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225, margin, 0),
('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875, margin, 0),
('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225, margin, 0),
('binance', False, 1, 2.0, 2.5, 0.0025, 75.8125, futures, 1),
('binance', False, 3, 2.0, 2.5, 0.0025, 73.8125, futures, -1),
('binance', True, 3, 2.0, 2.5, 0.0025, 74.1875, futures, 1),
('binance', True, 1, 2.0, 2.5, 0.0025, 76.1875, futures, -1),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, open_rate,
exchange, is_short, lev, close_rate, fee_rate, result):
def test_calc_close_trade_price(
limit_buy_order_usdt, limit_sell_order_usdt, open_rate, exchange, is_short,
lev, close_rate, fee_rate, result, trading_mode, funding_fees
):
trade = Trade(
pair='ADA/USDT',
stake_amount=60.0,
@ -731,46 +777,82 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, ope
exchange=exchange,
interest_rate=0.0005,
is_short=is_short,
leverage=lev
leverage=lev,
trading_mode=trading_mode,
funding_fees=funding_fees
)
trade.open_order_id = 'close_trade'
assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result
@pytest.mark.parametrize('exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio', [
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673),
('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402),
('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963),
('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789),
@pytest.mark.parametrize(
'exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio,trading_mode,funding_fees', [
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0),
('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402, margin, 0),
('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963, margin, 0),
('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789, margin, 0),
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632),
('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513),
('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395),
('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819),
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0),
('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513, margin, 0),
('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395, margin, 0),
('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819, margin, 0),
('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232),
('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534),
('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292),
('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876),
('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0),
('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534, margin, 0),
('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292, margin, 0),
('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876, margin, 0),
('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673),
('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248),
('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152),
('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455),
# # Kraken
('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0),
('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248, margin, 0),
('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152, margin, 0),
('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455, margin, 0),
('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632),
('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667),
('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334),
('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002),
('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0),
('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667, margin, 0),
('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334, margin, 0),
('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002, margin, 0),
('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232),
('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419),
('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614),
('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842),
('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0),
('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419, margin, 0),
('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614, margin, 0),
('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842, margin, 0),
('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927),
('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293),
('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565),
('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927, spot, 0),
('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293, spot, 0),
('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565, spot, 0),
# # FUTURES, funding_fee=1
('binance', False, 1, 2.1, 0.0025, 3.6925, 0.06138819617622615, futures, 1),
('binance', False, 3, 2.1, 0.0025, 3.6925, 0.18416458852867845, futures, 1),
('binance', True, 1, 2.1, 0.0025, -2.3074999999999974, -0.038554720133667564, futures, 1),
('binance', True, 3, 2.1, 0.0025, -2.3074999999999974, -0.11566416040100269, futures, 1),
('binance', False, 1, 1.9, 0.0025, -2.2925, -0.0381130507065669, futures, 1),
('binance', False, 3, 1.9, 0.0025, -2.2925, -0.1143391521197007, futures, 1),
('binance', True, 1, 1.9, 0.0025, 3.707500000000003, 0.06194653299916464, futures, 1),
('binance', True, 3, 1.9, 0.0025, 3.707500000000003, 0.18583959899749392, futures, 1),
('binance', False, 1, 2.2, 0.0025, 6.685, 0.11113881961762262, futures, 1),
('binance', False, 3, 2.2, 0.0025, 6.685, 0.33341645885286786, futures, 1),
('binance', True, 1, 2.2, 0.0025, -5.315000000000005, -0.08880534670008355, futures, 1),
('binance', True, 3, 2.2, 0.0025, -5.315000000000005, -0.26641604010025066, futures, 1),
# FUTURES, funding_fee=-1
('binance', False, 1, 2.1, 0.0025, 1.6925000000000026, 0.028137988362427313, futures, -1),
('binance', False, 3, 2.1, 0.0025, 1.6925000000000026, 0.08441396508728194, futures, -1),
('binance', True, 1, 2.1, 0.0025, -4.307499999999997, -0.07197159565580624, futures, -1),
('binance', True, 3, 2.1, 0.0025, -4.307499999999997, -0.21591478696741873, futures, -1),
('binance', False, 1, 1.9, 0.0025, -4.292499999999997, -0.07136325852036574, futures, -1),
('binance', False, 3, 1.9, 0.0025, -4.292499999999997, -0.2140897755610972, futures, -1),
('binance', True, 1, 1.9, 0.0025, 1.7075000000000031, 0.02852965747702596, futures, -1),
('binance', True, 3, 1.9, 0.0025, 1.7075000000000031, 0.08558897243107788, futures, -1),
('binance', False, 1, 2.2, 0.0025, 4.684999999999995, 0.07788861180382378, futures, -1),
('binance', False, 3, 2.2, 0.0025, 4.684999999999995, 0.23366583541147135, futures, -1),
('binance', True, 1, 2.2, 0.0025, -7.315000000000005, -0.12222222222222223, futures, -1),
('binance', True, 3, 2.2, 0.0025, -7.315000000000005, -0.3666666666666667, futures, -1),
])
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit(
@ -783,7 +865,9 @@ def test_calc_profit(
close_rate,
fee_close,
profit,
profit_ratio
profit_ratio,
trading_mode,
funding_fees
):
"""
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
@ -802,6 +886,7 @@ def test_calc_profit(
1x,-1x: 60.0 quote
3x,-3x: 20.0 quote
hours: 1/6 (10 minutes)
funding_fees: 1
borrowed
1x: 0 quote
3x: 40 quote
@ -913,6 +998,87 @@ def test_calc_profit(
2.1 quote: (62.811 / 60.15) - 1 = 0.04423940149625927
1.9 quote: (56.829 / 60.15) - 1 = -0.05521197007481293
2.2 quote: (65.802 / 60.15) - 1 = 0.09396508728179565
futures (live):
funding_fee: 1
close_value:
equations:
1x,3x: (amount * close_rate) - (amount * close_rate * fee) + funding_fees
-1x,-3x: (amount * close_rate) + (amount * close_rate * fee) - funding_fees
2.1 quote
1x,3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) + 1 = 63.8425
-1x,-3x: (30.00 * 2.1) + (30.00 * 2.1 * 0.0025) - 1 = 62.1575
1.9 quote
1x,3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) + 1 = 57.8575
-1x,-3x: (30.00 * 1.9) + (30.00 * 1.9 * 0.0025) - 1 = 56.1425
2.2 quote:
1x,3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) + 1 = 66.835
-1x,-3x: (30.00 * 2.20) + (30.00 * 2.20 * 0.0025) - 1 = 65.165
total_profit:
2.1 quote
1x,3x: 63.8425 - 60.15 = 3.6925
-1x,-3x: 59.850 - 62.1575 = -2.3074999999999974
1.9 quote
1x,3x: 57.8575 - 60.15 = -2.2925
-1x,-3x: 59.850 - 56.1425 = 3.707500000000003
2.2 quote:
1x,3x: 66.835 - 60.15 = 6.685
-1x,-3x: 59.850 - 65.165 = -5.315000000000005
total_profit_ratio:
2.1 quote
1x: (63.8425 / 60.15) - 1 = 0.06138819617622615
3x: ((63.8425 / 60.15) - 1)*3 = 0.18416458852867845
-1x: 1 - (62.1575 / 59.850) = -0.038554720133667564
-3x: (1 - (62.1575 / 59.850))*3 = -0.11566416040100269
1.9 quote
1x: (57.8575 / 60.15) - 1 = -0.0381130507065669
3x: ((57.8575 / 60.15) - 1)*3 = -0.1143391521197007
-1x: 1 - (56.1425 / 59.850) = 0.06194653299916464
-3x: (1 - (56.1425 / 59.850))*3 = 0.18583959899749392
2.2 quote
1x: (66.835 / 60.15) - 1 = 0.11113881961762262
3x: ((66.835 / 60.15) - 1)*3 = 0.33341645885286786
-1x: 1 - (65.165 / 59.850) = -0.08880534670008355
-3x: (1 - (65.165 / 59.850))*3 = -0.26641604010025066
funding_fee: -1
close_value:
equations:
(amount * close_rate) - (amount * close_rate * fee) + funding_fees
(amount * close_rate) - (amount * close_rate * fee) - funding_fees
2.1 quote
1x,3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) + (-1) = 61.8425
-1x,-3x: (30.00 * 2.1) + (30.00 * 2.1 * 0.0025) - (-1) = 64.1575
1.9 quote
1x,3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) + (-1) = 55.8575
-1x,-3x: (30.00 * 1.9) + (30.00 * 1.9 * 0.0025) - (-1) = 58.1425
2.2 quote:
1x,3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) + (-1) = 64.835
-1x,-3x: (30.00 * 2.20) + (30.00 * 2.20 * 0.0025) - (-1) = 67.165
total_profit:
2.1 quote
1x,3x: 61.8425 - 60.15 = 1.6925000000000026
-1x,-3x: 59.850 - 64.1575 = -4.307499999999997
1.9 quote
1x,3x: 55.8575 - 60.15 = -4.292499999999997
-1x,-3x: 59.850 - 58.1425 = 1.7075000000000031
2.2 quote:
1x,3x: 64.835 - 60.15 = 4.684999999999995
-1x,-3x: 59.850 - 67.165 = -7.315000000000005
total_profit_ratio:
2.1 quote
1x: (61.8425 / 60.15) - 1 = 0.028137988362427313
3x: ((61.8425 / 60.15) - 1)*3 = 0.08441396508728194
-1x: 1 - (64.1575 / 59.850) = -0.07197159565580624
-3x: (1 - (64.1575 / 59.850))*3 = -0.21591478696741873
1.9 quote
1x: (55.8575 / 60.15) - 1 = -0.07136325852036574
3x: ((55.8575 / 60.15) - 1)*3 = -0.2140897755610972
-1x: 1 - (58.1425 / 59.850) = 0.02852965747702596
-3x: (1 - (58.1425 / 59.850))*3 = 0.08558897243107788
2.2 quote
1x: (64.835 / 60.15) - 1 = 0.07788861180382378
3x: ((64.835 / 60.15) - 1)*3 = 0.23366583541147135
-1x: 1 - (67.165 / 59.850) = -0.12222222222222223
-3x: (1 - (67.165 / 59.850))*3 = -0.3666666666666667
"""
trade = Trade(
pair='ADA/USDT',
@ -925,7 +1091,9 @@ def test_calc_profit(
is_short=is_short,
leverage=lev,
fee_open=0.0025,
fee_close=fee_close
fee_close=fee_close,
trading_mode=trading_mode,
funding_fees=funding_fees
)
trade.open_order_id = 'something'
@ -1440,6 +1608,8 @@ def test_to_json(default_conf, fee):
'interest_rate': None,
'isolated_liq': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None
}
# Simulate dry_run entries
@ -1511,6 +1681,8 @@ def test_to_json(default_conf, fee):
'interest_rate': None,
'isolated_liq': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None
}