Merge branch 'develop' into pr/imxuwang/3799
This commit is contained in:
commit
7a8b274a44
@ -7,8 +7,8 @@ services:
|
|||||||
dockerfile: ".devcontainer/Dockerfile"
|
dockerfile: ".devcontainer/Dockerfile"
|
||||||
volumes:
|
volumes:
|
||||||
# Allow git usage within container
|
# Allow git usage within container
|
||||||
- "/home/${USER}/.ssh:/home/ftuser/.ssh:ro"
|
- "${HOME}/.ssh:/home/ftuser/.ssh:ro"
|
||||||
- "/home/${USER}/.gitconfig:/home/ftuser/.gitconfig:ro"
|
- "${HOME}/.gitconfig:/home/ftuser/.gitconfig:ro"
|
||||||
- ..:/freqtrade:cached
|
- ..:/freqtrade:cached
|
||||||
# Persist bash-history
|
# Persist bash-history
|
||||||
- freqtrade-vscode-server:/home/ftuser/.vscode-server
|
- freqtrade-vscode-server:/home/ftuser/.vscode-server
|
||||||
|
@ -59,8 +59,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
||||||
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
||||||
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
|
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
|
||||||
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled).
|
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled).
|
||||||
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
mkdocs-material==6.1.0
|
mkdocs-material==6.1.5
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==8.0.1
|
pymdown-extensions==8.0.1
|
||||||
|
@ -704,7 +704,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
|||||||
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
|
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Locking pairs is not functioning during backtesting.
|
Locking pairs is not available during backtesting.
|
||||||
|
|
||||||
#### Pair locking example
|
#### Pair locking example
|
||||||
|
|
||||||
|
@ -35,12 +35,30 @@ Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it fo
|
|||||||
|
|
||||||
Don't forget to start the conversation with your bot, by clicking `/START` button
|
Don't forget to start the conversation with your bot, by clicking `/START` button
|
||||||
|
|
||||||
### 2. Get your user id
|
### 2. Telegram user_id
|
||||||
|
|
||||||
|
#### Get your user id
|
||||||
|
|
||||||
Talk to the [userinfobot](https://telegram.me/userinfobot)
|
Talk to the [userinfobot](https://telegram.me/userinfobot)
|
||||||
|
|
||||||
Get your "Id", you will use it for the config parameter `chat_id`.
|
Get your "Id", you will use it for the config parameter `chat_id`.
|
||||||
|
|
||||||
|
#### Use Group id
|
||||||
|
|
||||||
|
You can use bots in telegram groups by just adding them to the group. You can find the group id by first adding a [RawDataBot](https://telegram.me/rawdatabot) to your group. The Group id is shown as id in the `"chat"` section, which the RawDataBot will send to you:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"chat":{
|
||||||
|
"id":-1001332619709
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"chat_id": "-1001332619709"
|
||||||
|
```
|
||||||
|
|
||||||
## Control telegram noise
|
## Control telegram noise
|
||||||
|
|
||||||
Freqtrade provides means to control the verbosity of your telegram bot.
|
Freqtrade provides means to control the verbosity of your telegram bot.
|
||||||
|
@ -32,7 +32,7 @@ python -m venv .env
|
|||||||
.env\Scripts\activate.ps1
|
.env\Scripts\activate.ps1
|
||||||
# optionally install ta-lib from wheel
|
# optionally install ta-lib from wheel
|
||||||
# Eventually adjust the below filename to match the downloaded wheel
|
# Eventually adjust the below filename to match the downloaded wheel
|
||||||
pip install build_helpes/TA_Lib‑0.4.19‑cp38‑cp38‑win_amd64.whl
|
pip install build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
freqtrade
|
freqtrade
|
||||||
@ -50,8 +50,8 @@ freqtrade
|
|||||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||||
```
|
```
|
||||||
|
|
||||||
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||||
|
|
||||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
|
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -4,6 +4,7 @@ Definition of cli arguments used in arguments.py
|
|||||||
from argparse import ArgumentTypeError
|
from argparse import ArgumentTypeError
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
|
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
||||||
|
|
||||||
|
|
||||||
def check_int_positive(value: str) -> int:
|
def check_int_positive(value: str) -> int:
|
||||||
@ -257,8 +258,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
||||||
'Different functions can generate completely different results, '
|
'Different functions can generate completely different results, '
|
||||||
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
||||||
'ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, '
|
f'{", ".join(HYPEROPT_LOSS_BUILTIN)}',
|
||||||
'SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily.',
|
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
),
|
),
|
||||||
"hyperoptexportfilename": Arg(
|
"hyperoptexportfilename": Arg(
|
||||||
@ -354,13 +354,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
'--data-format-ohlcv',
|
'--data-format-ohlcv',
|
||||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
default='json'
|
|
||||||
),
|
),
|
||||||
"dataformat_trades": Arg(
|
"dataformat_trades": Arg(
|
||||||
'--data-format-trades',
|
'--data-format-trades',
|
||||||
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
default='jsongz'
|
|
||||||
),
|
),
|
||||||
"exchange": Arg(
|
"exchange": Arg(
|
||||||
'--exchange',
|
'--exchange',
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import arrow
|
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||||
@ -29,12 +28,15 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
"You can only specify one or the other.")
|
"You can only specify one or the other.")
|
||||||
timerange = TimeRange()
|
timerange = TimeRange()
|
||||||
if 'days' in config:
|
if 'days' in config:
|
||||||
time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d")
|
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||||
|
|
||||||
if 'timerange' in config:
|
if 'timerange' in config:
|
||||||
timerange = timerange.parse_timerange(config['timerange'])
|
timerange = timerange.parse_timerange(config['timerange'])
|
||||||
|
|
||||||
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
|
config['stake_currency'] = ''
|
||||||
|
|
||||||
if 'pairs' not in config:
|
if 'pairs' not in config:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Downloading data requires a list of pairs. "
|
"Downloading data requires a list of pairs. "
|
||||||
|
@ -133,7 +133,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
if new_path.exists():
|
if new_path.exists():
|
||||||
raise OperationalException(f"`{new_path}` already exists. "
|
raise OperationalException(f"`{new_path}` already exists. "
|
||||||
"Please choose another Strategy Name.")
|
"Please choose another Hyperopt Name.")
|
||||||
deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
|
deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
|
||||||
else:
|
else:
|
||||||
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")
|
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")
|
||||||
|
@ -52,11 +52,11 @@ class TimeRange:
|
|||||||
:return: None (Modifies the object in place)
|
:return: None (Modifies the object in place)
|
||||||
"""
|
"""
|
||||||
if (not self.starttype or (startup_candles
|
if (not self.starttype or (startup_candles
|
||||||
and min_date.timestamp >= self.startts)):
|
and min_date.int_timestamp >= self.startts)):
|
||||||
# If no startts was defined, or backtest-data starts at the defined backtest-date
|
# If no startts was defined, or backtest-data starts at the defined backtest-date
|
||||||
logger.warning("Moving start-date by %s candles to account for startup time.",
|
logger.warning("Moving start-date by %s candles to account for startup time.",
|
||||||
startup_candles)
|
startup_candles)
|
||||||
self.startts = (min_date.timestamp + timeframe_secs * startup_candles)
|
self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles)
|
||||||
self.starttype = 'date'
|
self.starttype = 'date'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -89,7 +89,7 @@ class TimeRange:
|
|||||||
if stype[0]:
|
if stype[0]:
|
||||||
starts = rvals[index]
|
starts = rvals[index]
|
||||||
if stype[0] == 'date' and len(starts) == 8:
|
if stype[0] == 'date' and len(starts) == 8:
|
||||||
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
start = arrow.get(starts, 'YYYYMMDD').int_timestamp
|
||||||
elif len(starts) == 13:
|
elif len(starts) == 13:
|
||||||
start = int(starts) // 1000
|
start = int(starts) // 1000
|
||||||
else:
|
else:
|
||||||
@ -98,7 +98,7 @@ class TimeRange:
|
|||||||
if stype[1]:
|
if stype[1]:
|
||||||
stops = rvals[index]
|
stops = rvals[index]
|
||||||
if stype[1] == 'date' and len(stops) == 8:
|
if stype[1] == 'date' and len(stops) == 8:
|
||||||
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
stop = arrow.get(stops, 'YYYYMMDD').int_timestamp
|
||||||
elif len(stops) == 13:
|
elif len(stops) == 13:
|
||||||
stop = int(stops) // 1000
|
stop = int(stops) // 1000
|
||||||
else:
|
else:
|
||||||
|
@ -20,6 +20,9 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
|||||||
ORDERBOOK_SIDES = ['ask', 'bid']
|
ORDERBOOK_SIDES = ['ask', 'bid']
|
||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
|
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||||
|
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||||
|
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||||
'AgeFilter', 'PrecisionFilter', 'PriceFilter',
|
'AgeFilter', 'PrecisionFilter', 'PriceFilter',
|
||||||
'ShuffleFilter', 'SpreadFilter']
|
'ShuffleFilter', 'SpreadFilter']
|
||||||
|
@ -8,7 +8,6 @@ import logging
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from arrow import Arrow
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||||
@ -38,7 +37,7 @@ class DataProvider:
|
|||||||
:param timeframe: Timeframe to get data for
|
:param timeframe: Timeframe to get data for
|
||||||
:param dataframe: analyzed dataframe
|
:param dataframe: analyzed dataframe
|
||||||
"""
|
"""
|
||||||
self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime)
|
self.__cached_pairs[(pair, timeframe)] = (dataframe, datetime.now(timezone.utc))
|
||||||
|
|
||||||
def add_pairlisthandler(self, pairlists) -> None:
|
def add_pairlisthandler(self, pairlists) -> None:
|
||||||
"""
|
"""
|
||||||
@ -88,7 +87,8 @@ class DataProvider:
|
|||||||
"""
|
"""
|
||||||
return load_pair_history(pair=pair,
|
return load_pair_history(pair=pair,
|
||||||
timeframe=timeframe or self._config['timeframe'],
|
timeframe=timeframe or self._config['timeframe'],
|
||||||
datadir=self._config['datadir']
|
datadir=self._config['datadir'],
|
||||||
|
data_format=self._config.get('dataformat_ohlcv', 'json')
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
||||||
|
@ -3,6 +3,7 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
@ -175,7 +176,8 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == 'date':
|
||||||
where.append(f"timestamp < {timerange.stopts * 1e3}")
|
where.append(f"timestamp < {timerange.stopts * 1e3}")
|
||||||
|
|
||||||
trades = pd.read_hdf(filename, key=key, mode="r", where=where)
|
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||||
|
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||||
return trades.values.tolist()
|
return trades.values.tolist()
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
def trades_purge(self, pair: str) -> bool:
|
||||||
|
@ -87,7 +87,7 @@ class Edge:
|
|||||||
heartbeat = self.edge_config.get('process_throttle_secs')
|
heartbeat = self.edge_config.get('process_throttle_secs')
|
||||||
|
|
||||||
if (self._last_updated > 0) and (
|
if (self._last_updated > 0) and (
|
||||||
self._last_updated + heartbeat > arrow.utcnow().timestamp):
|
self._last_updated + heartbeat > arrow.utcnow().int_timestamp):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
data: Dict[str, Any] = {}
|
data: Dict[str, Any] = {}
|
||||||
@ -146,7 +146,7 @@ class Edge:
|
|||||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||||
self._cached_pairs = self._process_expectancy(trades_df)
|
self._cached_pairs = self._process_expectancy(trades_df)
|
||||||
self._last_updated = arrow.utcnow().timestamp
|
self._last_updated = arrow.utcnow().int_timestamp
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ class Exchange:
|
|||||||
asyncio.get_event_loop().run_until_complete(
|
asyncio.get_event_loop().run_until_complete(
|
||||||
self._api_async.load_markets(reload=reload))
|
self._api_async.load_markets(reload=reload))
|
||||||
|
|
||||||
except ccxt.BaseError as e:
|
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
||||||
logger.warning('Could not load async markets. Reason: %s', e)
|
logger.warning('Could not load async markets. Reason: %s', e)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -291,7 +291,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
self._api.load_markets()
|
self._api.load_markets()
|
||||||
self._load_async_markets()
|
self._load_async_markets()
|
||||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
logger.warning('Unable to initialize markets. Reason: %s', e)
|
logger.warning('Unable to initialize markets. Reason: %s', e)
|
||||||
|
|
||||||
@ -300,14 +300,14 @@ class Exchange:
|
|||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded
|
||||||
if (self._last_markets_refresh > 0) and (
|
if (self._last_markets_refresh > 0) and (
|
||||||
self._last_markets_refresh + self.markets_refresh_interval
|
self._last_markets_refresh + self.markets_refresh_interval
|
||||||
> arrow.utcnow().timestamp):
|
> arrow.utcnow().int_timestamp):
|
||||||
return None
|
return None
|
||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
try:
|
try:
|
||||||
self._api.load_markets(reload=True)
|
self._api.load_markets(reload=True)
|
||||||
# Also reload async markets to avoid issues with newly listed pairs
|
# Also reload async markets to avoid issues with newly listed pairs
|
||||||
self._load_async_markets(reload=True)
|
self._load_async_markets(reload=True)
|
||||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception("Could not reload markets.")
|
logger.exception("Could not reload markets.")
|
||||||
|
|
||||||
@ -501,7 +501,7 @@ class Exchange:
|
|||||||
'side': side,
|
'side': side,
|
||||||
'remaining': _amount,
|
'remaining': _amount,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'timestamp': int(arrow.utcnow().timestamp * 1000),
|
'timestamp': int(arrow.utcnow().int_timestamp * 1000),
|
||||||
'status': "closed" if ordertype == "market" else "open",
|
'status': "closed" if ordertype == "market" else "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {}
|
'info': {}
|
||||||
@ -687,6 +687,9 @@ class Exchange:
|
|||||||
async def _async_get_historic_ohlcv(self, pair: str,
|
async def _async_get_historic_ohlcv(self, pair: str,
|
||||||
timeframe: str,
|
timeframe: str,
|
||||||
since_ms: int) -> List:
|
since_ms: int) -> List:
|
||||||
|
"""
|
||||||
|
Download historic ohlcv
|
||||||
|
"""
|
||||||
|
|
||||||
one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit
|
one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@ -696,15 +699,20 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
input_coroutines = [self._async_get_candle_history(
|
input_coroutines = [self._async_get_candle_history(
|
||||||
pair, timeframe, since) for since in
|
pair, timeframe, since) for since in
|
||||||
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
||||||
|
|
||||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||||
|
|
||||||
# Combine gathered results
|
# Combine gathered results
|
||||||
data: List = []
|
data: List = []
|
||||||
for p, timeframe, res in results:
|
for res in results:
|
||||||
|
if isinstance(res, Exception):
|
||||||
|
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||||
|
continue
|
||||||
|
# Deconstruct tuple if it's not an exception
|
||||||
|
p, _, new_data = res
|
||||||
if p == pair:
|
if p == pair:
|
||||||
data.extend(res)
|
data.extend(new_data)
|
||||||
# Sort data again after extending the result - above calls return in "async order"
|
# Sort data again after extending the result - above calls return in "async order"
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
||||||
@ -741,9 +749,8 @@ class Exchange:
|
|||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||||
continue
|
continue
|
||||||
pair = res[0]
|
# Deconstruct tuple (has 3 elements)
|
||||||
timeframe = res[1]
|
pair, timeframe, ticks = res
|
||||||
ticks = res[2]
|
|
||||||
# keeping last candle time as last refreshed time of the pair
|
# keeping last candle time as last refreshed time of the pair
|
||||||
if ticks:
|
if ticks:
|
||||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||||
@ -759,7 +766,7 @@ class Exchange:
|
|||||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||||
|
|
||||||
return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
|
return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
|
||||||
+ interval_in_sec) >= arrow.utcnow().timestamp)
|
+ interval_in_sec) >= arrow.utcnow().int_timestamp)
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
async def _async_get_candle_history(self, pair: str, timeframe: str,
|
async def _async_get_candle_history(self, pair: str, timeframe: str,
|
||||||
|
@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
@ -19,10 +19,10 @@ from freqtrade.data.dataprovider import DataProvider
|
|||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, PricingError)
|
InvalidOrderException, PricingError)
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
||||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||||
from freqtrade.persistence import Order, Trade, cleanup_db, init_db
|
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
@ -72,6 +72,8 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange)
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
|
|
||||||
|
PairLocks.timeframe = self.config['timeframe']
|
||||||
|
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
|
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||||
@ -345,7 +347,7 @@ class FreqtradeBot:
|
|||||||
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
||||||
if not whitelist:
|
if not whitelist:
|
||||||
logger.info("Active pair whitelist is empty.")
|
logger.info("Active pair whitelist is empty.")
|
||||||
else:
|
return trades_created
|
||||||
# Remove pairs for currently opened trades from the whitelist
|
# Remove pairs for currently opened trades from the whitelist
|
||||||
for trade in Trade.get_open_trades():
|
for trade in Trade.get_open_trades():
|
||||||
if trade.pair in whitelist:
|
if trade.pair in whitelist:
|
||||||
@ -355,7 +357,7 @@ class FreqtradeBot:
|
|||||||
if not whitelist:
|
if not whitelist:
|
||||||
logger.info("No currency pair in active pair whitelist, "
|
logger.info("No currency pair in active pair whitelist, "
|
||||||
"but checking to sell open trades.")
|
"but checking to sell open trades.")
|
||||||
else:
|
return trades_created
|
||||||
# Create entity and execute trade for each pair from whitelist
|
# Create entity and execute trade for each pair from whitelist
|
||||||
for pair in whitelist:
|
for pair in whitelist:
|
||||||
try:
|
try:
|
||||||
@ -937,7 +939,7 @@ class FreqtradeBot:
|
|||||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||||
stoploss_order=True)
|
stoploss_order=True)
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
self._notify_sell(trade, "stoploss")
|
self._notify_sell(trade, "stoploss")
|
||||||
return True
|
return True
|
||||||
@ -1264,7 +1266,7 @@ class FreqtradeBot:
|
|||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
|
|
||||||
self._notify_sell(trade, order_type)
|
self._notify_sell(trade, order_type)
|
||||||
|
@ -340,7 +340,7 @@ class Backtesting:
|
|||||||
# max_open_trades must be respected
|
# max_open_trades must be respected
|
||||||
# don't open on the last row
|
# don't open on the last row
|
||||||
if ((position_stacking or len(open_trades[pair]) == 0)
|
if ((position_stacking or len(open_trades[pair]) == 0)
|
||||||
and max_open_trades > 0 and open_trade_count_start < max_open_trades
|
and (max_open_trades <= 0 or open_trade_count_start < max_open_trades)
|
||||||
and tmp != end_date
|
and tmp != end_date
|
||||||
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1):
|
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1):
|
||||||
# Enter trade
|
# Enter trade
|
||||||
|
@ -268,9 +268,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
'profit_total': results['profit_percent'].sum(),
|
'profit_total': results['profit_percent'].sum(),
|
||||||
'profit_total_abs': results['profit_abs'].sum(),
|
'profit_total_abs': results['profit_abs'].sum(),
|
||||||
'backtest_start': min_date.datetime,
|
'backtest_start': min_date.datetime,
|
||||||
'backtest_start_ts': min_date.timestamp * 1000,
|
'backtest_start_ts': min_date.int_timestamp * 1000,
|
||||||
'backtest_end': max_date.datetime,
|
'backtest_end': max_date.datetime,
|
||||||
'backtest_end_ts': max_date.timestamp * 1000,
|
'backtest_end_ts': max_date.int_timestamp * 1000,
|
||||||
'backtest_days': backtest_days,
|
'backtest_days': backtest_days,
|
||||||
|
|
||||||
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
|
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
|
|
||||||
from freqtrade.persistence.models import (Order, PairLock, Trade, clean_dry_run_db, cleanup_db,
|
from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db
|
||||||
init_db)
|
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||||
|
@ -270,7 +270,6 @@ class Trade(_DECL_BASE):
|
|||||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||||
'stake_amount': round(self.stake_amount, 8),
|
'stake_amount': round(self.stake_amount, 8),
|
||||||
'strategy': self.strategy,
|
'strategy': self.strategy,
|
||||||
'ticker_interval': self.timeframe, # DEPRECATED
|
|
||||||
'timeframe': self.timeframe,
|
'timeframe': self.timeframe,
|
||||||
|
|
||||||
'fee_open': self.fee_open,
|
'fee_open': self.fee_open,
|
||||||
@ -295,12 +294,16 @@ class Trade(_DECL_BASE):
|
|||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
||||||
'close_rate': self.close_rate,
|
'close_rate': self.close_rate,
|
||||||
'close_rate_requested': self.close_rate_requested,
|
'close_rate_requested': self.close_rate_requested,
|
||||||
'close_profit': self.close_profit,
|
'close_profit': self.close_profit, # Deprecated
|
||||||
'close_profit_abs': self.close_profit_abs,
|
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||||
|
'close_profit_abs': self.close_profit_abs, # Deprecated
|
||||||
|
|
||||||
|
'profit_ratio': self.close_profit,
|
||||||
|
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||||
|
'profit_abs': self.close_profit_abs,
|
||||||
|
|
||||||
'sell_reason': self.sell_reason,
|
'sell_reason': self.sell_reason,
|
||||||
'sell_order_status': self.sell_order_status,
|
'sell_order_status': self.sell_order_status,
|
||||||
'stop_loss': self.stop_loss, # Deprecated - should not be used
|
|
||||||
'stop_loss_abs': self.stop_loss,
|
'stop_loss_abs': self.stop_loss,
|
||||||
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
||||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||||
@ -309,7 +312,6 @@ class Trade(_DECL_BASE):
|
|||||||
if self.stoploss_last_update else None),
|
if self.stoploss_last_update else None),
|
||||||
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
|
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
|
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
|
||||||
'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used
|
|
||||||
'initial_stop_loss_abs': self.initial_stop_loss,
|
'initial_stop_loss_abs': self.initial_stop_loss,
|
||||||
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
||||||
if self.initial_stop_loss_pct else None),
|
if self.initial_stop_loss_pct else None),
|
||||||
@ -684,70 +686,21 @@ class PairLock(_DECL_BASE):
|
|||||||
f'lock_end_time={lock_end_time})')
|
f'lock_end_time={lock_end_time})')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def lock_pair(pair: str, until: datetime, reason: str = None) -> None:
|
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
|
||||||
lock = PairLock(
|
|
||||||
pair=pair,
|
|
||||||
lock_time=datetime.now(timezone.utc),
|
|
||||||
lock_end_time=until,
|
|
||||||
reason=reason,
|
|
||||||
active=True
|
|
||||||
)
|
|
||||||
PairLock.session.add(lock)
|
|
||||||
PairLock.session.flush()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List['PairLock']:
|
|
||||||
"""
|
"""
|
||||||
Get all locks for this pair
|
Get all locks for this pair
|
||||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
defaults to datetime.utcnow()
|
|
||||||
"""
|
"""
|
||||||
if not now:
|
|
||||||
now = datetime.now(timezone.utc)
|
|
||||||
|
|
||||||
filters = [func.datetime(PairLock.lock_end_time) >= now,
|
filters = [PairLock.lock_end_time > now,
|
||||||
# Only active locks
|
# Only active locks
|
||||||
PairLock.active.is_(True), ]
|
PairLock.active.is_(True), ]
|
||||||
if pair:
|
if pair:
|
||||||
filters.append(PairLock.pair == pair)
|
filters.append(PairLock.pair == pair)
|
||||||
return PairLock.query.filter(
|
return PairLock.query.filter(
|
||||||
*filters
|
*filters
|
||||||
).all()
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def unlock_pair(pair: str, now: Optional[datetime] = None) -> None:
|
|
||||||
"""
|
|
||||||
Release all locks for this pair.
|
|
||||||
:param pair: Pair to unlock
|
|
||||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
|
||||||
defaults to datetime.utcnow()
|
|
||||||
"""
|
|
||||||
if not now:
|
|
||||||
now = datetime.now(timezone.utc)
|
|
||||||
|
|
||||||
logger.info(f"Releasing all locks for {pair}.")
|
|
||||||
locks = PairLock.get_pair_locks(pair, now)
|
|
||||||
for lock in locks:
|
|
||||||
lock.active = False
|
|
||||||
PairLock.session.flush()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool:
|
|
||||||
"""
|
|
||||||
:param pair: Pair to check for
|
|
||||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
|
||||||
defaults to datetime.utcnow()
|
|
||||||
"""
|
|
||||||
if not now:
|
|
||||||
now = datetime.now(timezone.utc)
|
|
||||||
|
|
||||||
return PairLock.query.filter(
|
|
||||||
PairLock.pair == pair,
|
|
||||||
func.datetime(PairLock.lock_end_time) >= now,
|
|
||||||
# Only active locks
|
|
||||||
PairLock.active.is_(True),
|
|
||||||
).first() is not None
|
|
||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
99
freqtrade/persistence/pairlock_middleware.py
Normal file
99
freqtrade/persistence/pairlock_middleware.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from freqtrade.exchange import timeframe_to_next_date
|
||||||
|
from freqtrade.persistence.models import PairLock
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PairLocks():
|
||||||
|
"""
|
||||||
|
Pairlocks middleware class
|
||||||
|
Abstracts the database layer away so it becomes optional - which will be necessary to support
|
||||||
|
backtesting and hyperopt in the future.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use_db = True
|
||||||
|
locks: List[PairLock] = []
|
||||||
|
|
||||||
|
timeframe: str = ''
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def lock_pair(pair: str, until: datetime, reason: str = None) -> None:
|
||||||
|
lock = PairLock(
|
||||||
|
pair=pair,
|
||||||
|
lock_time=datetime.now(timezone.utc),
|
||||||
|
lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until),
|
||||||
|
reason=reason,
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
if PairLocks.use_db:
|
||||||
|
PairLock.session.add(lock)
|
||||||
|
PairLock.session.flush()
|
||||||
|
else:
|
||||||
|
PairLocks.locks.append(lock)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
||||||
|
"""
|
||||||
|
Get all currently active locks for this pair
|
||||||
|
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||||
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
|
defaults to datetime.now(timezone.utc)
|
||||||
|
"""
|
||||||
|
if not now:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
if PairLocks.use_db:
|
||||||
|
return PairLock.query_pair_locks(pair, now).all()
|
||||||
|
else:
|
||||||
|
locks = [lock for lock in PairLocks.locks if (
|
||||||
|
lock.lock_end_time >= now
|
||||||
|
and lock.active is True
|
||||||
|
and (pair is None or lock.pair == pair)
|
||||||
|
)]
|
||||||
|
return locks
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unlock_pair(pair: str, now: Optional[datetime] = None) -> None:
|
||||||
|
"""
|
||||||
|
Release all locks for this pair.
|
||||||
|
:param pair: Pair to unlock
|
||||||
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
|
defaults to datetime.now(timezone.utc)
|
||||||
|
"""
|
||||||
|
if not now:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
logger.info(f"Releasing all locks for {pair}.")
|
||||||
|
locks = PairLocks.get_pair_locks(pair, now)
|
||||||
|
for lock in locks:
|
||||||
|
lock.active = False
|
||||||
|
if PairLocks.use_db:
|
||||||
|
PairLock.session.flush()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_global_lock(now: Optional[datetime] = None) -> bool:
|
||||||
|
"""
|
||||||
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
|
defaults to datetime.now(timezone.utc)
|
||||||
|
"""
|
||||||
|
if not now:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
return len(PairLocks.get_pair_locks('*', now)) > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool:
|
||||||
|
"""
|
||||||
|
:param pair: Pair to check for
|
||||||
|
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||||
|
defaults to datetime.now(timezone.utc)
|
||||||
|
"""
|
||||||
|
if not now:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now)
|
@ -9,9 +9,9 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframe
|
|||||||
create_cum_profit, extract_trades_of_period, load_trades)
|
create_cum_profit, extract_trades_of_period, load_trades)
|
||||||
from freqtrade.data.converter import trim_dataframe
|
from freqtrade.data.converter import trim_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import get_timerange, load_data
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds
|
||||||
from freqtrade.misc import pair_to_filename
|
from freqtrade.misc import pair_to_filename
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import IStrategy
|
||||||
@ -29,7 +29,7 @@ except ImportError:
|
|||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def init_plotscript(config):
|
def init_plotscript(config, startup_candles: int = 0):
|
||||||
"""
|
"""
|
||||||
Initialize objects needed for plotting
|
Initialize objects needed for plotting
|
||||||
:return: Dict with candle (OHLCV) data, trades and pairs
|
:return: Dict with candle (OHLCV) data, trades and pairs
|
||||||
@ -48,9 +48,16 @@ def init_plotscript(config):
|
|||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
timeframe=config.get('timeframe', '5m'),
|
timeframe=config.get('timeframe', '5m'),
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
|
startup_candles=startup_candles,
|
||||||
data_format=config.get('dataformat_ohlcv', 'json'),
|
data_format=config.get('dataformat_ohlcv', 'json'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if startup_candles:
|
||||||
|
min_date, max_date = get_timerange(data)
|
||||||
|
logger.info(f"Loading data from {min_date} to {max_date}")
|
||||||
|
timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')),
|
||||||
|
startup_candles, min_date)
|
||||||
|
|
||||||
no_trades = False
|
no_trades = False
|
||||||
filename = config.get('exportfilename')
|
filename = config.get('exportfilename')
|
||||||
if config.get('no_trades', False):
|
if config.get('no_trades', False):
|
||||||
@ -72,6 +79,7 @@ def init_plotscript(config):
|
|||||||
return {"ohlcv": data,
|
return {"ohlcv": data,
|
||||||
"trades": trades,
|
"trades": trades,
|
||||||
"pairs": pairs,
|
"pairs": pairs,
|
||||||
|
"timerange": timerange,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -474,7 +482,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||||
IStrategy.dp = DataProvider(config, exchange)
|
IStrategy.dp = DataProvider(config, exchange)
|
||||||
plot_elements = init_plotscript(config)
|
plot_elements = init_plotscript(config, strategy.startup_candle_count)
|
||||||
|
timerange = plot_elements['timerange']
|
||||||
trades = plot_elements['trades']
|
trades = plot_elements['trades']
|
||||||
pair_counter = 0
|
pair_counter = 0
|
||||||
for pair, data in plot_elements["ohlcv"].items():
|
for pair, data in plot_elements["ohlcv"].items():
|
||||||
@ -482,6 +491,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
logger.info("analyse pair %s", pair)
|
logger.info("analyse pair %s", pair)
|
||||||
|
|
||||||
df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
|
df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
|
||||||
|
df_analyzed = trim_dataframe(df_analyzed, timerange)
|
||||||
trades_pair = trades.loc[trades['pair'] == pair]
|
trades_pair = trades.loc[trades['pair'] == pair]
|
||||||
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
|
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from freqtrade.constants import USERPATH_HYPEROPTS
|
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||||
@ -72,8 +72,11 @@ class HyperOptLossResolver(IResolver):
|
|||||||
|
|
||||||
hyperoptloss_name = config.get('hyperopt_loss')
|
hyperoptloss_name = config.get('hyperopt_loss')
|
||||||
if not hyperoptloss_name:
|
if not hyperoptloss_name:
|
||||||
raise OperationalException("No Hyperopt loss set. Please use `--hyperopt-loss` to "
|
raise OperationalException(
|
||||||
"specify the Hyperopt-Loss class to use.")
|
"No Hyperopt loss set. Please use `--hyperopt-loss` to "
|
||||||
|
"specify the Hyperopt-Loss class to use.\n"
|
||||||
|
f"Built-in Hyperopt-loss-functions are: {', '.join(HYPEROPT_LOSS_BUILTIN)}"
|
||||||
|
)
|
||||||
hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name,
|
hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name,
|
||||||
config, kwargs={},
|
config, kwargs={},
|
||||||
extra_dir=config.get('hyperopt_path'))
|
extra_dir=config.get('hyperopt_path'))
|
||||||
|
@ -329,7 +329,7 @@ class ApiServer(RPC):
|
|||||||
"""
|
"""
|
||||||
Prints the bot's version
|
Prints the bot's version
|
||||||
"""
|
"""
|
||||||
return jsonify(self._rpc_show_config(self._config))
|
return jsonify(RPC._rpc_show_config(self._config, self._freqtrade.state))
|
||||||
|
|
||||||
@require_login
|
@require_login
|
||||||
@rpc_catch_errors
|
@rpc_catch_errors
|
||||||
|
@ -19,7 +19,7 @@ from freqtrade.exceptions import ExchangeError, PricingError
|
|||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||||
from freqtrade.loggers import bufferHandler
|
from freqtrade.loggers import bufferHandler
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
from freqtrade.persistence import PairLock, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
@ -93,7 +93,8 @@ class RPC:
|
|||||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||||
""" Sends a message to all registered rpc modules """
|
""" Sends a message to all registered rpc modules """
|
||||||
|
|
||||||
def _rpc_show_config(self, config) -> Dict[str, Any]:
|
@staticmethod
|
||||||
|
def _rpc_show_config(config, botstate: State) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Return a dict of config options.
|
Return a dict of config options.
|
||||||
Explicitly does NOT return the full config to avoid leakage of sensitive
|
Explicitly does NOT return the full config to avoid leakage of sensitive
|
||||||
@ -104,22 +105,24 @@ class RPC:
|
|||||||
'stake_currency': config['stake_currency'],
|
'stake_currency': config['stake_currency'],
|
||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
'max_open_trades': config['max_open_trades'],
|
'max_open_trades': config['max_open_trades'],
|
||||||
'minimal_roi': config['minimal_roi'].copy(),
|
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
|
||||||
'stoploss': config['stoploss'],
|
'stoploss': config.get('stoploss'),
|
||||||
'trailing_stop': config['trailing_stop'],
|
'trailing_stop': config.get('trailing_stop'),
|
||||||
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
||||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
||||||
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
|
||||||
'ticker_interval': config['timeframe'], # DEPRECATED
|
'timeframe': config.get('timeframe'),
|
||||||
'timeframe': config['timeframe'],
|
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
||||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']),
|
) if 'timeframe' in config else '',
|
||||||
'timeframe_min': timeframe_to_minutes(config['timeframe']),
|
'timeframe_min': timeframe_to_minutes(config['timeframe']
|
||||||
|
) if 'timeframe' in config else '',
|
||||||
'exchange': config['exchange']['name'],
|
'exchange': config['exchange']['name'],
|
||||||
'strategy': config['strategy'],
|
'strategy': config['strategy'],
|
||||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||||
'ask_strategy': config.get('ask_strategy', {}),
|
'ask_strategy': config.get('ask_strategy', {}),
|
||||||
'bid_strategy': config.get('bid_strategy', {}),
|
'bid_strategy': config.get('bid_strategy', {}),
|
||||||
'state': str(self._freqtrade.state) if self._freqtrade else '',
|
'state': str(botstate),
|
||||||
|
'runmode': config['runmode'].value
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@ -152,17 +155,18 @@ class RPC:
|
|||||||
stoploss_current_dist = trade.stop_loss - current_rate
|
stoploss_current_dist = trade.stop_loss - current_rate
|
||||||
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
|
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
|
||||||
|
|
||||||
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
|
||||||
if trade.close_profit is not None else None)
|
|
||||||
trade_dict = trade.to_json()
|
trade_dict = trade.to_json()
|
||||||
trade_dict.update(dict(
|
trade_dict.update(dict(
|
||||||
base_currency=self._freqtrade.config['stake_currency'],
|
base_currency=self._freqtrade.config['stake_currency'],
|
||||||
close_profit=trade.close_profit if trade.close_profit is not None else None,
|
close_profit=trade.close_profit if trade.close_profit is not None else None,
|
||||||
close_profit_pct=fmt_close_profit,
|
|
||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
current_profit=current_profit,
|
current_profit=current_profit, # Deprectated
|
||||||
current_profit_pct=round(current_profit * 100, 2),
|
current_profit_pct=round(current_profit * 100, 2), # Deprectated
|
||||||
current_profit_abs=current_profit_abs,
|
current_profit_abs=current_profit_abs, # Deprectated
|
||||||
|
profit_ratio=current_profit,
|
||||||
|
profit_pct=round(current_profit * 100, 2),
|
||||||
|
profit_abs=current_profit_abs,
|
||||||
|
|
||||||
stoploss_current_dist=stoploss_current_dist,
|
stoploss_current_dist=stoploss_current_dist,
|
||||||
stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
|
stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
|
||||||
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
|
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
|
||||||
@ -601,10 +605,8 @@ class RPC:
|
|||||||
|
|
||||||
def _rpc_locks(self) -> Dict[str, Any]:
|
def _rpc_locks(self) -> Dict[str, Any]:
|
||||||
""" Returns the current locks"""
|
""" Returns the current locks"""
|
||||||
if self._freqtrade.state != State.RUNNING:
|
|
||||||
raise RPCException('trader is not running')
|
|
||||||
|
|
||||||
locks = PairLock.get_pair_locks(None)
|
locks = PairLocks.get_pair_locks(None)
|
||||||
return {
|
return {
|
||||||
'lock_count': len(locks),
|
'lock_count': len(locks),
|
||||||
'locks': [lock.to_json() for lock in locks]
|
'locks': [lock.to_json() for lock in locks]
|
||||||
|
@ -248,18 +248,17 @@ class Telegram(RPC):
|
|||||||
"*Open Rate:* `{open_rate:.8f}`",
|
"*Open Rate:* `{open_rate:.8f}`",
|
||||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||||
"*Current Rate:* `{current_rate:.8f}`",
|
"*Current Rate:* `{current_rate:.8f}`",
|
||||||
("*Close Profit:* `{close_profit_pct}`"
|
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
||||||
if r['close_profit_pct'] is not None else ""),
|
+ "`{profit_pct:.2f}%`",
|
||||||
"*Current Profit:* `{current_profit_pct:.2f}%`",
|
|
||||||
]
|
]
|
||||||
if (r['stop_loss'] != r['initial_stop_loss']
|
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
||||||
and r['initial_stop_loss_pct'] is not None):
|
and r['initial_stop_loss_pct'] is not None):
|
||||||
# Adding initial stoploss only if it is different from stoploss
|
# Adding initial stoploss only if it is different from stoploss
|
||||||
lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` "
|
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
||||||
"`({initial_stop_loss_pct:.2f}%)`")
|
"`({initial_stop_loss_pct:.2f}%)`")
|
||||||
|
|
||||||
# Adding stoploss and stoploss percentage only if it is not None
|
# Adding stoploss and stoploss percentage only if it is not None
|
||||||
lines.append("*Stoploss:* `{stop_loss:.8f}` " +
|
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
||||||
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
|
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
|
||||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||||
"`({stoploss_current_dist_pct:.2f}%)`")
|
"`({stoploss_current_dist_pct:.2f}%)`")
|
||||||
@ -833,7 +832,8 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
val = self._rpc_show_config(self._freqtrade.config)
|
val = RPC._rpc_show_config(self._freqtrade.config, self._freqtrade.state)
|
||||||
|
|
||||||
if val['trailing_stop']:
|
if val['trailing_stop']:
|
||||||
sl_info = (
|
sl_info = (
|
||||||
f"*Initial Stoploss:* `{val['stoploss']}`\n"
|
f"*Initial Stoploss:* `{val['stoploss']}`\n"
|
||||||
|
@ -17,7 +17,7 @@ from freqtrade.data.dataprovider import DataProvider
|
|||||||
from freqtrade.exceptions import OperationalException, StrategyError
|
from freqtrade.exceptions import OperationalException, StrategyError
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||||
from freqtrade.persistence import PairLock, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ class IStrategy(ABC):
|
|||||||
Needs to be timezone aware `datetime.now(timezone.utc)`
|
Needs to be timezone aware `datetime.now(timezone.utc)`
|
||||||
:param reason: Optional string explaining why the pair was locked.
|
:param reason: Optional string explaining why the pair was locked.
|
||||||
"""
|
"""
|
||||||
PairLock.lock_pair(pair, until, reason)
|
PairLocks.lock_pair(pair, until, reason)
|
||||||
|
|
||||||
def unlock_pair(self, pair: str) -> None:
|
def unlock_pair(self, pair: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -297,7 +297,7 @@ class IStrategy(ABC):
|
|||||||
manually from within the strategy, to allow an easy way to unlock pairs.
|
manually from within the strategy, to allow an easy way to unlock pairs.
|
||||||
:param pair: Unlock pair to allow trading again
|
:param pair: Unlock pair to allow trading again
|
||||||
"""
|
"""
|
||||||
PairLock.unlock_pair(pair, datetime.now(timezone.utc))
|
PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
|
||||||
|
|
||||||
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
|
def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -312,10 +312,10 @@ class IStrategy(ABC):
|
|||||||
|
|
||||||
if not candle_date:
|
if not candle_date:
|
||||||
# Simple call ...
|
# Simple call ...
|
||||||
return PairLock.is_pair_locked(pair, candle_date)
|
return PairLocks.is_pair_locked(pair, candle_date)
|
||||||
else:
|
else:
|
||||||
lock_time = timeframe_to_next_date(self.timeframe, candle_date)
|
lock_time = timeframe_to_next_date(self.timeframe, candle_date)
|
||||||
return PairLock.is_pair_locked(pair, lock_time)
|
return PairLocks.is_pair_locked(pair, lock_time)
|
||||||
|
|
||||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
@ -63,7 +63,7 @@ class {{ strategy }}(IStrategy):
|
|||||||
ignore_roi_if_buy_signal = False
|
ignore_roi_if_buy_signal = False
|
||||||
|
|
||||||
# Number of candles the strategy requires before producing valid signals
|
# Number of candles the strategy requires before producing valid signals
|
||||||
startup_candle_count: int = 20
|
startup_candle_count: int = 30
|
||||||
|
|
||||||
# Optional order type mapping.
|
# Optional order type mapping.
|
||||||
order_types = {
|
order_types = {
|
||||||
|
@ -64,7 +64,7 @@ class SampleStrategy(IStrategy):
|
|||||||
ignore_roi_if_buy_signal = False
|
ignore_roi_if_buy_signal = False
|
||||||
|
|
||||||
# Number of candles the strategy requires before producing valid signals
|
# Number of candles the strategy requires before producing valid signals
|
||||||
startup_candle_count: int = 20
|
startup_candle_count: int = 30
|
||||||
|
|
||||||
# Optional order type mapping.
|
# Optional order type mapping.
|
||||||
order_types = {
|
order_types = {
|
||||||
|
@ -108,13 +108,13 @@ class Wallets:
|
|||||||
for trading operations, the latest balance is needed.
|
for trading operations, the latest balance is needed.
|
||||||
:param require_update: Allow skipping an update if balances were recently refreshed
|
:param require_update: Allow skipping an update if balances were recently refreshed
|
||||||
"""
|
"""
|
||||||
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().timestamp)):
|
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)):
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
self._update_dry()
|
self._update_dry()
|
||||||
else:
|
else:
|
||||||
self._update_live()
|
self._update_live()
|
||||||
logger.info('Wallets synced.')
|
logger.info('Wallets synced.')
|
||||||
self._last_wallet_refresh = arrow.utcnow().timestamp
|
self._last_wallet_refresh = arrow.utcnow().int_timestamp
|
||||||
|
|
||||||
def get_all_balances(self) -> Dict[str, Any]:
|
def get_all_balances(self) -> Dict[str, Any]:
|
||||||
return self._wallets
|
return self._wallets
|
||||||
|
@ -8,7 +8,7 @@ flake8==3.8.4
|
|||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==4.1.0
|
flake8-tidy-imports==4.1.0
|
||||||
mypy==0.790
|
mypy==0.790
|
||||||
pytest==6.1.1
|
pytest==6.1.2
|
||||||
pytest-asyncio==0.14.0
|
pytest-asyncio==0.14.0
|
||||||
pytest-cov==2.10.1
|
pytest-cov==2.10.1
|
||||||
pytest-mock==3.3.1
|
pytest-mock==3.3.1
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.5.3
|
scipy==1.5.4
|
||||||
scikit-learn==0.23.2
|
scikit-learn==0.23.2
|
||||||
scikit-optimize==0.8.1
|
scikit-optimize==0.8.1
|
||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==4.11.0
|
plotly==4.12.0
|
||||||
|
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
numpy==1.19.2
|
numpy==1.19.4
|
||||||
pandas==1.1.3
|
pandas==1.1.4
|
||||||
|
|
||||||
ccxt==1.36.66
|
ccxt==1.37.69
|
||||||
multidict==4.7.6
|
aiohttp==3.7.2
|
||||||
aiohttp==3.6.3
|
|
||||||
SQLAlchemy==1.3.20
|
SQLAlchemy==1.3.20
|
||||||
python-telegram-bot==13.0
|
python-telegram-bot==13.0
|
||||||
arrow==0.17.0
|
arrow==0.17.0
|
||||||
cachetools==4.1.1
|
cachetools==4.1.1
|
||||||
requests==2.24.0
|
requests==2.25.0
|
||||||
urllib3==1.25.10
|
urllib3==1.26.2
|
||||||
wrapt==1.12.1
|
wrapt==1.12.1
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
TA-Lib==0.4.19
|
TA-Lib==0.4.19
|
||||||
@ -23,18 +22,18 @@ blosc==1.9.2
|
|||||||
py_find_1st==1.1.4
|
py_find_1st==1.1.4
|
||||||
|
|
||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==0.9.1
|
python-rapidjson==0.9.3
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# Api server
|
# Api server
|
||||||
flask==1.1.2
|
flask==1.1.2
|
||||||
flask-jwt-extended==3.24.1
|
flask-jwt-extended==3.25.0
|
||||||
flask-cors==3.0.9
|
flask-cors==3.0.9
|
||||||
|
|
||||||
# Support for colorized terminal output
|
# Support for colorized terminal output
|
||||||
colorama==0.4.4
|
colorama==0.4.4
|
||||||
# Building config files interactively
|
# Building config files interactively
|
||||||
questionary==1.7.0
|
questionary==1.8.0
|
||||||
prompt-toolkit==3.0.8
|
prompt-toolkit==3.0.8
|
||||||
|
2
setup.py
2
setup.py
@ -69,7 +69,7 @@ setup(name='freqtrade',
|
|||||||
'ccxt>=1.24.96',
|
'ccxt>=1.24.96',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot',
|
'python-telegram-bot',
|
||||||
'arrow',
|
'arrow>=0.17.0',
|
||||||
'cachetools',
|
'cachetools',
|
||||||
'requests',
|
'requests',
|
||||||
'urllib3',
|
'urllib3',
|
||||||
|
@ -435,6 +435,16 @@ def test_list_markets(mocker, markets, capsys):
|
|||||||
assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE)
|
assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE)
|
||||||
assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE)
|
assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError))
|
||||||
|
# Test --one-column
|
||||||
|
args = [
|
||||||
|
"list-markets",
|
||||||
|
'--config', 'config.json.example',
|
||||||
|
"--one-column"
|
||||||
|
]
|
||||||
|
with pytest.raises(OperationalException, match=r"Cannot get markets.*"):
|
||||||
|
start_list_markets(get_args(args), False)
|
||||||
|
|
||||||
|
|
||||||
def test_create_datadir_failed(caplog):
|
def test_create_datadir_failed(caplog):
|
||||||
|
|
||||||
@ -476,6 +486,12 @@ def test_start_new_strategy(mocker, caplog):
|
|||||||
assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0]
|
assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0]
|
||||||
assert log_has_re("Writing strategy to .*", caplog)
|
assert log_has_re("Writing strategy to .*", caplog)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration')
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r".* already exists. Please choose another Strategy Name\."):
|
||||||
|
start_new_strategy(get_args(args))
|
||||||
|
|
||||||
|
|
||||||
def test_start_new_strategy_DefaultStrat(mocker, caplog):
|
def test_start_new_strategy_DefaultStrat(mocker, caplog):
|
||||||
args = [
|
args = [
|
||||||
@ -512,6 +528,12 @@ def test_start_new_hyperopt(mocker, caplog):
|
|||||||
assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0]
|
assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0]
|
||||||
assert log_has_re("Writing hyperopt to .*", caplog)
|
assert log_has_re("Writing hyperopt to .*", caplog)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration')
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r".* already exists. Please choose another Hyperopt Name\."):
|
||||||
|
start_new_hyperopt(get_args(args))
|
||||||
|
|
||||||
|
|
||||||
def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog):
|
def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog):
|
||||||
args = [
|
args = [
|
||||||
@ -579,7 +601,7 @@ def test_download_data_timerange(mocker, caplog, markets):
|
|||||||
start_download_data(get_args(args))
|
start_download_data(get_args(args))
|
||||||
assert dl_mock.call_count == 1
|
assert dl_mock.call_count == 1
|
||||||
# 20days ago
|
# 20days ago
|
||||||
days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).timestamp
|
days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp
|
||||||
assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago
|
assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago
|
||||||
|
|
||||||
dl_mock.reset_mock()
|
dl_mock.reset_mock()
|
||||||
@ -592,7 +614,8 @@ def test_download_data_timerange(mocker, caplog, markets):
|
|||||||
start_download_data(get_args(args))
|
start_download_data(get_args(args))
|
||||||
assert dl_mock.call_count == 1
|
assert dl_mock.call_count == 1
|
||||||
|
|
||||||
assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(2020, 1, 1).timestamp
|
assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(
|
||||||
|
2020, 1, 1).int_timestamp
|
||||||
|
|
||||||
|
|
||||||
def test_download_data_no_markets(mocker, caplog):
|
def test_download_data_no_markets(mocker, caplog):
|
||||||
@ -695,6 +718,7 @@ def test_start_list_strategies(mocker, caplog, capsys):
|
|||||||
"list-strategies",
|
"list-strategies",
|
||||||
"--strategy-path",
|
"--strategy-path",
|
||||||
str(Path(__file__).parent.parent / "strategy" / "strats"),
|
str(Path(__file__).parent.parent / "strategy" / "strats"),
|
||||||
|
'--no-color',
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
# pargs['config'] = None
|
# pargs['config'] = None
|
||||||
@ -769,6 +793,25 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
|||||||
assert re.match(r"Pairs for .*", captured.out)
|
assert re.match(r"Pairs for .*", captured.out)
|
||||||
assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out)
|
assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'test-pairlist',
|
||||||
|
'-c', 'config.json.example',
|
||||||
|
'--one-column',
|
||||||
|
]
|
||||||
|
start_test_pairlist(get_args(args))
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert re.match(r"ETH/BTC\nTKN/BTC\nBLK/BTC\nLTC/BTC\nXRP/BTC\n", captured.out)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'test-pairlist',
|
||||||
|
'-c', 'config.json.example',
|
||||||
|
'--print-json',
|
||||||
|
]
|
||||||
|
start_test_pairlist(get_args(args))
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert re.match(r'Pairs for BTC: \n\["ETH/BTC","TKN/BTC","BLK/BTC","LTC/BTC","XRP/BTC"\]\n',
|
||||||
|
captured.out)
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
@ -792,7 +792,7 @@ def limit_buy_order_open():
|
|||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'symbol': 'mocked',
|
'symbol': 'mocked',
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'timestamp': arrow.utcnow().timestamp,
|
'timestamp': arrow.utcnow().int_timestamp,
|
||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
@ -911,7 +911,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': '1234512345',
|
'id': '1234512345',
|
||||||
'clientOrderId': None,
|
'clientOrderId': None,
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
@ -932,7 +932,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': 'AZNPFF-4AC4N-7MKTAT',
|
'id': 'AZNPFF-4AC4N-7MKTAT',
|
||||||
'clientOrderId': None,
|
'clientOrderId': None,
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'status': 'canceled',
|
'status': 'canceled',
|
||||||
@ -953,7 +953,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': '1234512345',
|
'id': '1234512345',
|
||||||
'clientOrderId': 'alb1234123',
|
'clientOrderId': 'alb1234123',
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
@ -974,7 +974,7 @@ def limit_buy_order_canceled_empty(request):
|
|||||||
'info': {},
|
'info': {},
|
||||||
'id': '1234512345',
|
'id': '1234512345',
|
||||||
'clientOrderId': 'alb1234123',
|
'clientOrderId': 'alb1234123',
|
||||||
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
|
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp,
|
||||||
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
'lastTradeTimestamp': None,
|
'lastTradeTimestamp': None,
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
@ -1000,7 +1000,7 @@ def limit_sell_order_open():
|
|||||||
'side': 'sell',
|
'side': 'sell',
|
||||||
'pair': 'mocked',
|
'pair': 'mocked',
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'timestamp': arrow.utcnow().timestamp,
|
'timestamp': arrow.utcnow().int_timestamp,
|
||||||
'price': 0.00001173,
|
'price': 0.00001173,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
|
@ -52,6 +52,31 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history):
|
|||||||
assert historymock.call_args_list[0][1]["timeframe"] == "5m"
|
assert historymock.call_args_list[0][1]["timeframe"] == "5m"
|
||||||
|
|
||||||
|
|
||||||
|
def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history):
|
||||||
|
hdf5loadmock = MagicMock(return_value=ohlcv_history)
|
||||||
|
jsonloadmock = MagicMock(return_value=ohlcv_history)
|
||||||
|
mocker.patch("freqtrade.data.history.hdf5datahandler.HDF5DataHandler._ohlcv_load", hdf5loadmock)
|
||||||
|
mocker.patch("freqtrade.data.history.jsondatahandler.JsonDataHandler._ohlcv_load", jsonloadmock)
|
||||||
|
|
||||||
|
default_conf["runmode"] = RunMode.BACKTEST
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||||
|
assert isinstance(data, DataFrame)
|
||||||
|
hdf5loadmock.assert_not_called()
|
||||||
|
jsonloadmock.assert_called_once()
|
||||||
|
|
||||||
|
# Swiching to dataformat hdf5
|
||||||
|
hdf5loadmock.reset_mock()
|
||||||
|
jsonloadmock.reset_mock()
|
||||||
|
default_conf["dataformat_ohlcv"] = "hdf5"
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||||
|
assert isinstance(data, DataFrame)
|
||||||
|
hdf5loadmock.assert_called_once()
|
||||||
|
jsonloadmock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
|
def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
|
||||||
default_conf["runmode"] = RunMode.DRY_RUN
|
default_conf["runmode"] = RunMode.DRY_RUN
|
||||||
timeframe = default_conf["timeframe"]
|
timeframe = default_conf["timeframe"]
|
||||||
|
@ -323,7 +323,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
|
|||||||
start = arrow.get('2018-01-01T00:00:00')
|
start = arrow.get('2018-01-01T00:00:00')
|
||||||
end = arrow.get('2018-01-11T00:00:00')
|
end = arrow.get('2018-01-11T00:00:00')
|
||||||
data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20,
|
data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20,
|
||||||
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
|
timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp))
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Using indicator startup period: 20 ...', caplog
|
'Using indicator startup period: 20 ...', caplog
|
||||||
)
|
)
|
||||||
@ -339,7 +339,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
|
|||||||
start = arrow.get('2018-01-10T00:00:00')
|
start = arrow.get('2018-01-10T00:00:00')
|
||||||
end = arrow.get('2018-02-20T00:00:00')
|
end = arrow.get('2018-02-20T00:00:00')
|
||||||
data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
||||||
timerange=TimeRange('date', 'date', start.timestamp, end.timestamp))
|
timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp))
|
||||||
# timedifference in 5 minutes
|
# timedifference in 5 minutes
|
||||||
td = ((end - start).total_seconds() // 60 // 5) + 1
|
td = ((end - start).total_seconds() // 60 // 5) + 1
|
||||||
assert td != len(data['UNITTEST/BTC'])
|
assert td != len(data['UNITTEST/BTC'])
|
||||||
@ -724,6 +724,8 @@ def test_hdf5datahandler_trades_load(testdatadir):
|
|||||||
|
|
||||||
trades2 = dh._trades_load('XRP/ETH', timerange)
|
trades2 = dh._trades_load('XRP/ETH', timerange)
|
||||||
assert len(trades) > len(trades2)
|
assert len(trades) > len(trades2)
|
||||||
|
# Check that ID is None (If it's nan, it's wrong)
|
||||||
|
assert trades2[0][2] is None
|
||||||
|
|
||||||
# unfiltered load has trades before starttime
|
# unfiltered load has trades before starttime
|
||||||
assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0
|
assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0
|
||||||
|
@ -50,7 +50,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
|
|||||||
'date': tests_start_time.shift(
|
'date': tests_start_time.shift(
|
||||||
minutes=(
|
minutes=(
|
||||||
ohlc[0] *
|
ohlc[0] *
|
||||||
timeframe_in_minute)).timestamp *
|
timeframe_in_minute)).int_timestamp *
|
||||||
1000,
|
1000,
|
||||||
'buy': ohlc[1],
|
'buy': ohlc[1],
|
||||||
'open': ohlc[2],
|
'open': ohlc[2],
|
||||||
@ -71,7 +71,7 @@ def _build_dataframe(buy_ohlc_sell_matrice):
|
|||||||
|
|
||||||
def _time_on_candle(number):
|
def _time_on_candle(number):
|
||||||
return np.datetime64(tests_start_time.shift(
|
return np.datetime64(tests_start_time.shift(
|
||||||
minutes=(number * timeframe_in_minute)).timestamp * 1000, 'ms')
|
minutes=(number * timeframe_in_minute)).int_timestamp * 1000, 'ms')
|
||||||
|
|
||||||
|
|
||||||
# End helper functions
|
# End helper functions
|
||||||
@ -251,7 +251,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf):
|
|||||||
heartbeat = edge_conf['edge']['process_throttle_secs']
|
heartbeat = edge_conf['edge']['process_throttle_secs']
|
||||||
|
|
||||||
# should not recalculate if heartbeat not reached
|
# should not recalculate if heartbeat not reached
|
||||||
edge._last_updated = arrow.utcnow().timestamp - heartbeat + 1
|
edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1
|
||||||
|
|
||||||
assert edge.calculate() is False
|
assert edge.calculate() is False
|
||||||
|
|
||||||
@ -263,7 +263,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
|||||||
|
|
||||||
NEOBTC = [
|
NEOBTC = [
|
||||||
[
|
[
|
||||||
tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000,
|
tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000,
|
||||||
math.sin(x * hz) / 1000 + base,
|
math.sin(x * hz) / 1000 + base,
|
||||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||||
@ -275,7 +275,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
|||||||
base = 0.002
|
base = 0.002
|
||||||
LTCBTC = [
|
LTCBTC = [
|
||||||
[
|
[
|
||||||
tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000,
|
tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000,
|
||||||
math.sin(x * hz) / 1000 + base,
|
math.sin(x * hz) / 1000 + base,
|
||||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||||
@ -299,7 +299,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
|
|||||||
|
|
||||||
assert edge.calculate()
|
assert edge.calculate()
|
||||||
assert len(edge._cached_pairs) == 2
|
assert len(edge._cached_pairs) == 2
|
||||||
assert edge._last_updated <= arrow.utcnow().timestamp + 2
|
assert edge._last_updated <= arrow.utcnow().int_timestamp + 2
|
||||||
|
|
||||||
|
|
||||||
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||||
|
|
||||||
@ -393,7 +393,7 @@ def test_reload_markets(default_conf, mocker, caplog):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
|
||||||
mock_markets=False)
|
mock_markets=False)
|
||||||
exchange._load_async_markets = MagicMock()
|
exchange._load_async_markets = MagicMock()
|
||||||
exchange._last_markets_refresh = arrow.utcnow().timestamp
|
exchange._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||||
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
|
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
|
||||||
|
|
||||||
assert exchange.markets == initial_markets
|
assert exchange.markets == initial_markets
|
||||||
@ -404,7 +404,7 @@ def test_reload_markets(default_conf, mocker, caplog):
|
|||||||
assert exchange._load_async_markets.call_count == 0
|
assert exchange._load_async_markets.call_count == 0
|
||||||
|
|
||||||
# more than 10 minutes have passed, reload is executed
|
# more than 10 minutes have passed, reload is executed
|
||||||
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
|
exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60
|
||||||
exchange.reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange.markets == updated_markets
|
assert exchange.markets == updated_markets
|
||||||
assert exchange._load_async_markets.call_count == 1
|
assert exchange._load_async_markets.call_count == 1
|
||||||
@ -1272,7 +1272,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
[
|
[
|
||||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
|
||||||
1, # open
|
1, # open
|
||||||
2, # high
|
2, # high
|
||||||
3, # low
|
3, # low
|
||||||
@ -1289,17 +1289,28 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
|||||||
# one_call calculation * 1.8 should do 2 calls
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
|
|
||||||
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
|
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
|
||||||
ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
ret = exchange.get_historic_ohlcv(pair, "5m", int((
|
||||||
|
arrow.utcnow().int_timestamp - since) * 1000))
|
||||||
|
|
||||||
assert exchange._async_get_candle_history.call_count == 2
|
assert exchange._async_get_candle_history.call_count == 2
|
||||||
# Returns twice the above OHLCV data
|
# Returns twice the above OHLCV data
|
||||||
assert len(ret) == 2
|
assert len(ret) == 2
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
async def mock_get_candle_hist_error(pair, *args, **kwargs):
|
||||||
|
raise TimeoutError()
|
||||||
|
|
||||||
|
exchange._async_get_candle_history = MagicMock(side_effect=mock_get_candle_hist_error)
|
||||||
|
ret = exchange.get_historic_ohlcv(pair, "5m", int(
|
||||||
|
(arrow.utcnow().int_timestamp - since) * 1000))
|
||||||
|
assert log_has_re(r"Async code raised an exception: .*", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
[
|
[
|
||||||
(arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms
|
(arrow.utcnow().int_timestamp - 1) * 1000, # unix timestamp ms
|
||||||
1, # open
|
1, # open
|
||||||
2, # high
|
2, # high
|
||||||
3, # low
|
3, # low
|
||||||
@ -1307,7 +1318,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
5, # volume (in quote currency)
|
5, # volume (in quote currency)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
|
||||||
3, # open
|
3, # open
|
||||||
1, # high
|
1, # high
|
||||||
4, # low
|
4, # low
|
||||||
@ -1353,7 +1364,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
[
|
[
|
||||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
|
||||||
1, # open
|
1, # open
|
||||||
2, # high
|
2, # high
|
||||||
3, # low
|
3, # low
|
||||||
@ -1388,14 +1399,14 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
|||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
await exchange._async_get_candle_history(pair, "5m",
|
await exchange._async_get_candle_history(pair, "5m",
|
||||||
(arrow.utcnow().timestamp - 2000) * 1000)
|
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
||||||
r'historical candle \(OHLCV\) data\..*'):
|
r'historical candle \(OHLCV\) data\..*'):
|
||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
await exchange._async_get_candle_history(pair, "5m",
|
await exchange._async_get_candle_history(pair, "5m",
|
||||||
(arrow.utcnow().timestamp - 2000) * 1000)
|
(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -1641,13 +1652,13 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
|||||||
with pytest.raises(OperationalException, match=r'Could not fetch trade data*'):
|
with pytest.raises(OperationalException, match=r'Could not fetch trade data*'):
|
||||||
api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000)
|
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
|
||||||
r'historical trade data\..*'):
|
r'historical trade data\..*'):
|
||||||
api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000)
|
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -2291,6 +2302,9 @@ def test_timeframe_to_next_date():
|
|||||||
date = datetime.now(tz=timezone.utc)
|
date = datetime.now(tz=timezone.utc)
|
||||||
assert timeframe_to_next_date("5m") > date
|
assert timeframe_to_next_date("5m") > date
|
||||||
|
|
||||||
|
date = datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)
|
||||||
|
assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [
|
@pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [
|
||||||
("BTC/USDT", 'BTC', 'USDT', "binance", {}, True),
|
("BTC/USDT", 'BTC', 'USDT', "binance", {}, True),
|
||||||
|
51
tests/optimize/conftest.py
Normal file
51
tests/optimize/conftest.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.optimize.hyperopt import Hyperopt
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from tests.conftest import patch_exchange
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def hyperopt_conf(default_conf):
|
||||||
|
hyperconf = deepcopy(default_conf)
|
||||||
|
hyperconf.update({
|
||||||
|
'hyperopt': 'DefaultHyperOpt',
|
||||||
|
'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
|
||||||
|
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
|
||||||
|
'epochs': 1,
|
||||||
|
'timerange': None,
|
||||||
|
'spaces': ['default'],
|
||||||
|
'hyperopt_jobs': 1,
|
||||||
|
})
|
||||||
|
return hyperconf
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def hyperopt(hyperopt_conf, mocker):
|
||||||
|
|
||||||
|
patch_exchange(mocker)
|
||||||
|
return Hyperopt(hyperopt_conf)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def hyperopt_results():
|
||||||
|
return pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||||
|
'profit_percent': [-0.1, 0.2, 0.3],
|
||||||
|
'profit_abs': [-0.2, 0.4, 0.6],
|
||||||
|
'trade_duration': [10, 30, 10],
|
||||||
|
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI],
|
||||||
|
'close_date':
|
||||||
|
[
|
||||||
|
datetime(2019, 1, 1, 9, 26, 3, 478039),
|
||||||
|
datetime(2019, 2, 1, 9, 26, 3, 478039),
|
||||||
|
datetime(2019, 3, 1, 9, 26, 3, 478039)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
@ -2,7 +2,6 @@
|
|||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
@ -17,58 +16,15 @@ from freqtrade import constants
|
|||||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss
|
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt
|
from freqtrade.optimize.hyperopt import Hyperopt
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.interface import SellType
|
|
||||||
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
||||||
patched_configuration_load_config_file)
|
patched_configuration_load_config_file)
|
||||||
|
|
||||||
from .hyperopts.default_hyperopt import DefaultHyperOpt
|
from .hyperopts.default_hyperopt import DefaultHyperOpt
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def hyperopt_conf(default_conf):
|
|
||||||
hyperconf = deepcopy(default_conf)
|
|
||||||
hyperconf.update({
|
|
||||||
'hyperopt': 'DefaultHyperOpt',
|
|
||||||
'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
|
|
||||||
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
|
|
||||||
'epochs': 1,
|
|
||||||
'timerange': None,
|
|
||||||
'spaces': ['default'],
|
|
||||||
'hyperopt_jobs': 1,
|
|
||||||
})
|
|
||||||
return hyperconf
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def hyperopt(hyperopt_conf, mocker):
|
|
||||||
|
|
||||||
patch_exchange(mocker)
|
|
||||||
return Hyperopt(hyperopt_conf)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def hyperopt_results():
|
|
||||||
return pd.DataFrame(
|
|
||||||
{
|
|
||||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
|
||||||
'profit_percent': [-0.1, 0.2, 0.3],
|
|
||||||
'profit_abs': [-0.2, 0.4, 0.6],
|
|
||||||
'trade_duration': [10, 30, 10],
|
|
||||||
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI],
|
|
||||||
'close_date':
|
|
||||||
[
|
|
||||||
datetime(2019, 1, 1, 9, 26, 3, 478039),
|
|
||||||
datetime(2019, 2, 1, 9, 26, 3, 478039),
|
|
||||||
datetime(2019, 3, 1, 9, 26, 3, 478039)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
def create_results(mocker, hyperopt, testdatadir) -> List[Dict]:
|
def create_results(mocker, hyperopt, testdatadir) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
@ -230,32 +186,6 @@ def test_hyperoptresolver_noname(default_conf):
|
|||||||
HyperOptResolver.load_hyperopt(default_conf)
|
HyperOptResolver.load_hyperopt(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptlossresolver_noname(default_conf):
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify "
|
|
||||||
"the Hyperopt-Loss class to use."):
|
|
||||||
HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptlossresolver(mocker, default_conf) -> None:
|
|
||||||
|
|
||||||
hl = ShortTradeDurHyperOptLoss
|
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object',
|
|
||||||
MagicMock(return_value=hl)
|
|
||||||
)
|
|
||||||
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
|
|
||||||
x = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
assert hasattr(x, "hyperopt_loss_function")
|
|
||||||
|
|
||||||
|
|
||||||
def test_hyperoptlossresolver_wrongname(default_conf) -> None:
|
|
||||||
default_conf.update({'hyperopt_loss': "NonExistingLossClass"})
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'):
|
|
||||||
HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
@ -269,7 +199,8 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None:
|
|||||||
'--hyperopt', 'DefaultHyperOpt',
|
'--hyperopt', 'DefaultHyperOpt',
|
||||||
'--hyperopt-path',
|
'--hyperopt-path',
|
||||||
str(Path(__file__).parent / "hyperopts"),
|
str(Path(__file__).parent / "hyperopts"),
|
||||||
'--epochs', '5'
|
'--epochs', '5',
|
||||||
|
'--hyperopt-loss', 'SharpeHyperOptLossDaily',
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
|
|
||||||
@ -337,137 +268,6 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None:
|
|||||||
assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog)
|
assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None:
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
|
||||||
correct = hl.hyperopt_loss_function(hyperopt_results, 600,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert over > correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None:
|
|
||||||
resultsb = hyperopt_results.copy()
|
|
||||||
resultsb.loc[1, 'trade_duration'] = 20
|
|
||||||
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
|
||||||
longer = hl.hyperopt_loss_function(hyperopt_results, 100,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
shorter = hl.hyperopt_loss_function(resultsb, 100,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert shorter < longer
|
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None:
|
|
||||||
results_over = hyperopt_results.copy()
|
|
||||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
|
||||||
results_under = hyperopt_results.copy()
|
|
||||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
|
||||||
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
|
||||||
correct = hl.hyperopt_loss_function(hyperopt_results, 600,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
over = hl.hyperopt_loss_function(results_over, 600,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
under = hl.hyperopt_loss_function(results_under, 600,
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert over < correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
|
||||||
results_over = hyperopt_results.copy()
|
|
||||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
|
||||||
results_under = hyperopt_results.copy()
|
|
||||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
|
||||||
|
|
||||||
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert over < correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
|
||||||
results_over = hyperopt_results.copy()
|
|
||||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
|
||||||
results_under = hyperopt_results.copy()
|
|
||||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
|
||||||
|
|
||||||
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert over < correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
|
||||||
results_over = hyperopt_results.copy()
|
|
||||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
|
||||||
results_under = hyperopt_results.copy()
|
|
||||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
|
||||||
|
|
||||||
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'})
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert over < correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
|
||||||
results_over = hyperopt_results.copy()
|
|
||||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
|
||||||
results_under = hyperopt_results.copy()
|
|
||||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
|
||||||
|
|
||||||
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'})
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert over < correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
|
||||||
results_over = hyperopt_results.copy()
|
|
||||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
|
||||||
results_under = hyperopt_results.copy()
|
|
||||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
|
||||||
|
|
||||||
default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
|
|
||||||
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
|
||||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
|
||||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
|
||||||
assert over < correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||||
hyperopt.current_best_loss = 2
|
hyperopt.current_best_loss = 2
|
||||||
hyperopt.total_epochs = 2
|
hyperopt.total_epochs = 2
|
||||||
|
165
tests/optimize/test_hyperoptloss.py
Normal file
165
tests/optimize/test_hyperoptloss.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss
|
||||||
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperoptlossresolver_noname(default_conf):
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify "
|
||||||
|
"the Hyperopt-Loss class to use."):
|
||||||
|
HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperoptlossresolver(mocker, default_conf) -> None:
|
||||||
|
|
||||||
|
hl = ShortTradeDurHyperOptLoss
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object',
|
||||||
|
MagicMock(return_value=hl)
|
||||||
|
)
|
||||||
|
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
|
||||||
|
x = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
assert hasattr(x, "hyperopt_loss_function")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperoptlossresolver_wrongname(default_conf) -> None:
|
||||||
|
default_conf.update({'hyperopt_loss': "NonExistingLossClass"})
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'):
|
||||||
|
HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None:
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, 600,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over > correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None:
|
||||||
|
resultsb = hyperopt_results.copy()
|
||||||
|
resultsb.loc[1, 'trade_duration'] = 20
|
||||||
|
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||||
|
longer = hl.hyperopt_loss_function(hyperopt_results, 100,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
shorter = hl.hyperopt_loss_function(resultsb, 100,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert shorter < longer
|
||||||
|
|
||||||
|
|
||||||
|
def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None:
|
||||||
|
results_over = hyperopt_results.copy()
|
||||||
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
results_under = hyperopt_results.copy()
|
||||||
|
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||||
|
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, 600,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(results_over, 600,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(results_under, 600,
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over < correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||||
|
results_over = hyperopt_results.copy()
|
||||||
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
results_under = hyperopt_results.copy()
|
||||||
|
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||||
|
|
||||||
|
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'})
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over < correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||||
|
results_over = hyperopt_results.copy()
|
||||||
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
results_under = hyperopt_results.copy()
|
||||||
|
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||||
|
|
||||||
|
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over < correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||||
|
results_over = hyperopt_results.copy()
|
||||||
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
results_under = hyperopt_results.copy()
|
||||||
|
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||||
|
|
||||||
|
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'})
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over < correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||||
|
results_over = hyperopt_results.copy()
|
||||||
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
results_under = hyperopt_results.copy()
|
||||||
|
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||||
|
|
||||||
|
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'})
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over < correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||||
|
results_over = hyperopt_results.copy()
|
||||||
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
results_under = hyperopt_results.copy()
|
||||||
|
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||||
|
|
||||||
|
default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over < correct
|
||||||
|
assert under > correct
|
82
tests/pairlist/test_pairlocks.py
Normal file
82
tests/pairlist/test_pairlocks.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.persistence import PairLocks
|
||||||
|
from freqtrade.persistence.models import PairLock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('use_db', (False, True))
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_PairLocks(use_db):
|
||||||
|
PairLocks.timeframe = '5m'
|
||||||
|
# No lock should be present
|
||||||
|
if use_db:
|
||||||
|
assert len(PairLock.query.all()) == 0
|
||||||
|
else:
|
||||||
|
PairLocks.use_db = False
|
||||||
|
|
||||||
|
assert PairLocks.use_db == use_db
|
||||||
|
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
assert not PairLocks.is_pair_locked(pair)
|
||||||
|
PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
||||||
|
# ETH/BTC locked for 4 minutes
|
||||||
|
assert PairLocks.is_pair_locked(pair)
|
||||||
|
|
||||||
|
# XRP/BTC should not be locked now
|
||||||
|
pair = 'XRP/BTC'
|
||||||
|
assert not PairLocks.is_pair_locked(pair)
|
||||||
|
# Unlocking a pair that's not locked should not raise an error
|
||||||
|
PairLocks.unlock_pair(pair)
|
||||||
|
|
||||||
|
PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
||||||
|
assert PairLocks.is_pair_locked(pair)
|
||||||
|
|
||||||
|
# Get both locks from above
|
||||||
|
locks = PairLocks.get_pair_locks(None)
|
||||||
|
assert len(locks) == 2
|
||||||
|
|
||||||
|
# Unlock original pair
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
PairLocks.unlock_pair(pair)
|
||||||
|
assert not PairLocks.is_pair_locked(pair)
|
||||||
|
assert not PairLocks.is_global_lock()
|
||||||
|
|
||||||
|
pair = 'BTC/USDT'
|
||||||
|
# Lock until 14:30
|
||||||
|
lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
|
||||||
|
PairLocks.lock_pair(pair, lock_time)
|
||||||
|
|
||||||
|
assert not PairLocks.is_pair_locked(pair)
|
||||||
|
assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-10))
|
||||||
|
assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-10))
|
||||||
|
assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50))
|
||||||
|
assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))
|
||||||
|
|
||||||
|
# Should not be locked after time expired
|
||||||
|
assert not PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=10))
|
||||||
|
|
||||||
|
locks = PairLocks.get_pair_locks(pair, lock_time + timedelta(minutes=-2))
|
||||||
|
assert len(locks) == 1
|
||||||
|
assert 'PairLock' in str(locks[0])
|
||||||
|
|
||||||
|
# Unlock all
|
||||||
|
PairLocks.unlock_pair(pair, lock_time + timedelta(minutes=-2))
|
||||||
|
assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))
|
||||||
|
|
||||||
|
# Global lock
|
||||||
|
PairLocks.lock_pair('*', lock_time)
|
||||||
|
assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50))
|
||||||
|
# Global lock also locks every pair seperately
|
||||||
|
assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50))
|
||||||
|
assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50))
|
||||||
|
|
||||||
|
if use_db:
|
||||||
|
assert len(PairLock.query.all()) > 0
|
||||||
|
else:
|
||||||
|
# Nothing was pushed to the database
|
||||||
|
assert len(PairLock.query.all()) == 0
|
||||||
|
# Reset use-db variable
|
||||||
|
PairLocks.use_db = True
|
@ -69,8 +69,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'min_rate': ANY,
|
'min_rate': ANY,
|
||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'strategy': ANY,
|
'strategy': ANY,
|
||||||
'ticker_interval': ANY,
|
'timeframe': 5,
|
||||||
'timeframe': ANY,
|
|
||||||
'open_order_id': ANY,
|
'open_order_id': ANY,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
'close_date_hum': None,
|
'close_date_hum': None,
|
||||||
@ -87,14 +86,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'current_profit': -0.00408133,
|
'current_profit': -0.00408133,
|
||||||
'current_profit_pct': -0.41,
|
'current_profit_pct': -0.41,
|
||||||
'current_profit_abs': -4.09e-06,
|
'current_profit_abs': -4.09e-06,
|
||||||
'stop_loss': 9.882e-06,
|
'profit_ratio': -0.00408133,
|
||||||
|
'profit_pct': -0.41,
|
||||||
|
'profit_abs': -4.09e-06,
|
||||||
'stop_loss_abs': 9.882e-06,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_pct': -10.0,
|
'stop_loss_pct': -10.0,
|
||||||
'stop_loss_ratio': -0.1,
|
'stop_loss_ratio': -0.1,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': ANY,
|
'stoploss_last_update': ANY,
|
||||||
'stoploss_last_update_timestamp': ANY,
|
'stoploss_last_update_timestamp': ANY,
|
||||||
'initial_stop_loss': 9.882e-06,
|
|
||||||
'initial_stop_loss_abs': 9.882e-06,
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
'initial_stop_loss_pct': -10.0,
|
'initial_stop_loss_pct': -10.0,
|
||||||
'initial_stop_loss_ratio': -0.1,
|
'initial_stop_loss_ratio': -0.1,
|
||||||
@ -134,7 +134,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'min_rate': ANY,
|
'min_rate': ANY,
|
||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'strategy': ANY,
|
'strategy': ANY,
|
||||||
'ticker_interval': ANY,
|
|
||||||
'timeframe': ANY,
|
'timeframe': ANY,
|
||||||
'open_order_id': ANY,
|
'open_order_id': ANY,
|
||||||
'close_date': None,
|
'close_date': None,
|
||||||
@ -152,14 +151,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'current_profit': ANY,
|
'current_profit': ANY,
|
||||||
'current_profit_pct': ANY,
|
'current_profit_pct': ANY,
|
||||||
'current_profit_abs': ANY,
|
'current_profit_abs': ANY,
|
||||||
'stop_loss': 9.882e-06,
|
'profit_ratio': ANY,
|
||||||
|
'profit_pct': ANY,
|
||||||
|
'profit_abs': ANY,
|
||||||
'stop_loss_abs': 9.882e-06,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_pct': -10.0,
|
'stop_loss_pct': -10.0,
|
||||||
'stop_loss_ratio': -0.1,
|
'stop_loss_ratio': -0.1,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': ANY,
|
'stoploss_last_update': ANY,
|
||||||
'stoploss_last_update_timestamp': ANY,
|
'stoploss_last_update_timestamp': ANY,
|
||||||
'initial_stop_loss': 9.882e-06,
|
|
||||||
'initial_stop_loss_abs': 9.882e-06,
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
'initial_stop_loss_pct': -10.0,
|
'initial_stop_loss_pct': -10.0,
|
||||||
'initial_stop_loss_ratio': -0.1,
|
'initial_stop_loss_ratio': -0.1,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Unit test file for rpc/api_server.py
|
Unit test file for rpc/api_server.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||||
|
|
||||||
@ -12,9 +12,9 @@ from requests.auth import _basic_auth_str
|
|||||||
|
|
||||||
from freqtrade.__init__ import __version__
|
from freqtrade.__init__ import __version__
|
||||||
from freqtrade.loggers import setup_logging, setup_logging_pre
|
from freqtrade.loggers import setup_logging, setup_logging_pre
|
||||||
from freqtrade.persistence import PairLock, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.rpc.api_server import BASE_URI, ApiServer
|
from freqtrade.rpc.api_server import BASE_URI, ApiServer
|
||||||
from freqtrade.state import State
|
from freqtrade.state import RunMode, State
|
||||||
from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal
|
from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal
|
||||||
|
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ _TEST_PASS = "SuperSecurePassword1!"
|
|||||||
def botclient(default_conf, mocker):
|
def botclient(default_conf, mocker):
|
||||||
setup_logging_pre()
|
setup_logging_pre()
|
||||||
setup_logging(default_conf)
|
setup_logging(default_conf)
|
||||||
|
default_conf['runmode'] = RunMode.DRY_RUN
|
||||||
default_conf.update({"api_server": {"enabled": True,
|
default_conf.update({"api_server": {"enabled": True,
|
||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
@ -339,8 +339,8 @@ def test_api_locks(botclient):
|
|||||||
assert rc.json['lock_count'] == 0
|
assert rc.json['lock_count'] == 0
|
||||||
assert rc.json['lock_count'] == len(rc.json['locks'])
|
assert rc.json['lock_count'] == len(rc.json['locks'])
|
||||||
|
|
||||||
PairLock.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason')
|
PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason')
|
||||||
PairLock.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef')
|
PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef')
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/locks")
|
rc = client_get(client, f"{BASE_URI}/locks")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
@ -360,7 +360,6 @@ def test_api_show_config(botclient, mocker):
|
|||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert 'dry_run' in rc.json
|
assert 'dry_run' in rc.json
|
||||||
assert rc.json['exchange'] == 'bittrex'
|
assert rc.json['exchange'] == 'bittrex'
|
||||||
assert rc.json['ticker_interval'] == '5m'
|
|
||||||
assert rc.json['timeframe'] == '5m'
|
assert rc.json['timeframe'] == '5m'
|
||||||
assert rc.json['timeframe_ms'] == 300000
|
assert rc.json['timeframe_ms'] == 300000
|
||||||
assert rc.json['timeframe_min'] == 5
|
assert rc.json['timeframe_min'] == 5
|
||||||
@ -639,6 +638,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'current_profit': -0.00408133,
|
'current_profit': -0.00408133,
|
||||||
'current_profit_pct': -0.41,
|
'current_profit_pct': -0.41,
|
||||||
'current_profit_abs': -4.09e-06,
|
'current_profit_abs': -4.09e-06,
|
||||||
|
'profit_ratio': -0.00408133,
|
||||||
|
'profit_pct': -0.41,
|
||||||
|
'profit_abs': -4.09e-06,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'open_date_hum': 'just now',
|
'open_date_hum': 'just now',
|
||||||
@ -647,14 +649,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'open_rate': 1.098e-05,
|
'open_rate': 1.098e-05,
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'stop_loss': 9.882e-06,
|
|
||||||
'stop_loss_abs': 9.882e-06,
|
'stop_loss_abs': 9.882e-06,
|
||||||
'stop_loss_pct': -10.0,
|
'stop_loss_pct': -10.0,
|
||||||
'stop_loss_ratio': -0.1,
|
'stop_loss_ratio': -0.1,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': ANY,
|
'stoploss_last_update': ANY,
|
||||||
'stoploss_last_update_timestamp': ANY,
|
'stoploss_last_update_timestamp': ANY,
|
||||||
'initial_stop_loss': 9.882e-06,
|
|
||||||
'initial_stop_loss_abs': 9.882e-06,
|
'initial_stop_loss_abs': 9.882e-06,
|
||||||
'initial_stop_loss_pct': -10.0,
|
'initial_stop_loss_pct': -10.0,
|
||||||
'initial_stop_loss_ratio': -0.1,
|
'initial_stop_loss_ratio': -0.1,
|
||||||
@ -682,7 +682,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': 'DefaultStrategy',
|
'strategy': 'DefaultStrategy',
|
||||||
'ticker_interval': 5,
|
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'bittrex',
|
'exchange': 'bittrex',
|
||||||
}]
|
}]
|
||||||
@ -779,20 +778,22 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'open_rate': 0.245441,
|
'open_rate': 0.245441,
|
||||||
'pair': 'ETH/ETH',
|
'pair': 'ETH/ETH',
|
||||||
'stake_amount': 1,
|
'stake_amount': 1,
|
||||||
'stop_loss': None,
|
|
||||||
'stop_loss_abs': None,
|
'stop_loss_abs': None,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': None,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_ratio': None,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_last_update': None,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update_timestamp': None,
|
||||||
'initial_stop_loss': None,
|
|
||||||
'initial_stop_loss_abs': None,
|
'initial_stop_loss_abs': None,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_pct': None,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_ratio': None,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
|
'close_profit_pct': None,
|
||||||
'close_profit_abs': None,
|
'close_profit_abs': None,
|
||||||
'close_rate_requested': None,
|
'close_rate_requested': None,
|
||||||
|
'profit_ratio': None,
|
||||||
|
'profit_pct': None,
|
||||||
|
'profit_abs': None,
|
||||||
'fee_close': 0.0025,
|
'fee_close': 0.0025,
|
||||||
'fee_close_cost': None,
|
'fee_close_cost': None,
|
||||||
'fee_close_currency': None,
|
'fee_close_currency': None,
|
||||||
@ -808,7 +809,6 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'ticker_interval': None,
|
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'bittrex',
|
'exchange': 'bittrex',
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,10 @@ from freqtrade.constants import CANCEL_REASON
|
|||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.loggers import setup_logging
|
from freqtrade.loggers import setup_logging
|
||||||
from freqtrade.persistence import PairLock, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from freqtrade.state import State
|
from freqtrade.state import RunMode, State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange,
|
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange,
|
||||||
patch_get_signal, patch_whitelist)
|
patch_get_signal, patch_whitelist)
|
||||||
@ -164,16 +164,17 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
|||||||
'amount': 90.99181074,
|
'amount': 90.99181074,
|
||||||
'stake_amount': 90.99181074,
|
'stake_amount': 90.99181074,
|
||||||
'close_profit_pct': None,
|
'close_profit_pct': None,
|
||||||
'current_profit': -0.0059,
|
'profit': -0.0059,
|
||||||
'current_profit_pct': -0.59,
|
'profit_pct': -0.59,
|
||||||
'initial_stop_loss': 1.098e-05,
|
'initial_stop_loss_abs': 1.098e-05,
|
||||||
'stop_loss': 1.099e-05,
|
'stop_loss_abs': 1.099e-05,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'initial_stop_loss_pct': -0.05,
|
'initial_stop_loss_pct': -0.05,
|
||||||
'stoploss_current_dist': 1e-08,
|
'stoploss_current_dist': 1e-08,
|
||||||
'stoploss_current_dist_pct': -0.02,
|
'stoploss_current_dist_pct': -0.02,
|
||||||
'stop_loss_pct': -0.01,
|
'stop_loss_pct': -0.01,
|
||||||
'open_order': '(limit buy rem=0.00000000)'
|
'open_order': '(limit buy rem=0.00000000)',
|
||||||
|
'is_open': True
|
||||||
}]),
|
}]),
|
||||||
_status_table=status_table,
|
_status_table=status_table,
|
||||||
_send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
@ -1041,15 +1042,8 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason')
|
||||||
telegram._locks(update=update, context=MagicMock())
|
PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef')
|
||||||
assert msg_mock.call_count == 1
|
|
||||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
|
||||||
msg_mock.reset_mock()
|
|
||||||
freqtradebot.state = State.RUNNING
|
|
||||||
|
|
||||||
PairLock.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason')
|
|
||||||
PairLock.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef')
|
|
||||||
|
|
||||||
telegram._locks(update=update, context=MagicMock())
|
telegram._locks(update=update, context=MagicMock())
|
||||||
|
|
||||||
@ -1309,6 +1303,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
|
|||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
_send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
|
default_conf['runmode'] = RunMode.DRY_RUN
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from freqtrade.configuration import TimeRange
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
from freqtrade.exceptions import StrategyError
|
from freqtrade.exceptions import StrategyError
|
||||||
from freqtrade.persistence import PairLock, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from tests.conftest import log_has, log_has_re
|
from tests.conftest import log_has, log_has_re
|
||||||
@ -362,13 +362,14 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
|
|||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_is_pair_locked(default_conf):
|
def test_is_pair_locked(default_conf):
|
||||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||||
|
PairLocks.timeframe = default_conf['timeframe']
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
# No lock should be present
|
# No lock should be present
|
||||||
assert len(PairLock.query.all()) == 0
|
assert len(PairLocks.get_pair_locks(None)) == 0
|
||||||
|
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
assert not strategy.is_pair_locked(pair)
|
assert not strategy.is_pair_locked(pair)
|
||||||
strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime)
|
||||||
# ETH/BTC locked for 4 minutes
|
# ETH/BTC locked for 4 minutes
|
||||||
assert strategy.is_pair_locked(pair)
|
assert strategy.is_pair_locked(pair)
|
||||||
|
|
||||||
@ -387,7 +388,8 @@ def test_is_pair_locked(default_conf):
|
|||||||
pair = 'BTC/USDT'
|
pair = 'BTC/USDT'
|
||||||
# Lock until 14:30
|
# Lock until 14:30
|
||||||
lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
|
lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
|
||||||
strategy.lock_pair(pair, lock_time)
|
# Subtract 2 seconds, as locking rounds up to the next candle.
|
||||||
|
strategy.lock_pair(pair, lock_time - timedelta(seconds=2))
|
||||||
|
|
||||||
assert not strategy.is_pair_locked(pair)
|
assert not strategy.is_pair_locked(pair)
|
||||||
# latest candle is from 14:20, lock goes to 14:30
|
# latest candle is from 14:20, lock goes to 14:30
|
||||||
|
@ -6,7 +6,7 @@ from unittest.mock import MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
from freqtrade.commands.cli_options import check_int_positive
|
from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive
|
||||||
|
|
||||||
|
|
||||||
# Parse common command-line-arguments. Used for all tools
|
# Parse common command-line-arguments. Used for all tools
|
||||||
@ -249,8 +249,31 @@ def test_check_int_positive() -> None:
|
|||||||
with pytest.raises(argparse.ArgumentTypeError):
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
check_int_positive('0')
|
check_int_positive('0')
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
check_int_positive(0)
|
||||||
|
|
||||||
with pytest.raises(argparse.ArgumentTypeError):
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
check_int_positive('3.5')
|
check_int_positive('3.5')
|
||||||
|
|
||||||
with pytest.raises(argparse.ArgumentTypeError):
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
check_int_positive('DeadBeef')
|
check_int_positive('DeadBeef')
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_int_nonzero() -> None:
|
||||||
|
assert check_int_nonzero('3') == 3
|
||||||
|
assert check_int_nonzero('1') == 1
|
||||||
|
assert check_int_nonzero('100') == 100
|
||||||
|
|
||||||
|
assert check_int_nonzero('-2') == -2
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
check_int_nonzero('0')
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
check_int_nonzero(0)
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
check_int_nonzero('3.5')
|
||||||
|
|
||||||
|
with pytest.raises(argparse.ArgumentTypeError):
|
||||||
|
check_int_nonzero('DeadBeef')
|
||||||
|
@ -15,7 +15,8 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie
|
|||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
TemporaryError)
|
TemporaryError)
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Order, PairLock, Trade
|
from freqtrade.persistence import Order, Trade
|
||||||
|
from freqtrade.persistence.models import PairLock
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.state import RunMode, State
|
from freqtrade.state import RunMode, State
|
||||||
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
@ -9,7 +8,7 @@ from sqlalchemy import create_engine
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.persistence import Order, PairLock, Trade, clean_dry_run_db, init_db
|
from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db
|
||||||
from tests.conftest import create_mock_trades, log_has, log_has_re
|
from tests.conftest import create_mock_trades, log_has, log_has_re
|
||||||
|
|
||||||
|
|
||||||
@ -817,24 +816,25 @@ def test_to_json(default_conf, fee):
|
|||||||
'amount_requested': 123.0,
|
'amount_requested': 123.0,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
|
'close_profit_pct': None,
|
||||||
'close_profit_abs': None,
|
'close_profit_abs': None,
|
||||||
|
'profit_ratio': None,
|
||||||
|
'profit_pct': None,
|
||||||
|
'profit_abs': None,
|
||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'stop_loss': None,
|
|
||||||
'stop_loss_abs': None,
|
'stop_loss_abs': None,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_ratio': None,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': None,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_last_update': None,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update_timestamp': None,
|
||||||
'initial_stop_loss': None,
|
|
||||||
'initial_stop_loss_abs': None,
|
'initial_stop_loss_abs': None,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_pct': None,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_ratio': None,
|
||||||
'min_rate': None,
|
'min_rate': None,
|
||||||
'max_rate': None,
|
'max_rate': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'ticker_interval': None,
|
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'bittrex',
|
'exchange': 'bittrex',
|
||||||
}
|
}
|
||||||
@ -869,19 +869,21 @@ def test_to_json(default_conf, fee):
|
|||||||
'amount': 100.0,
|
'amount': 100.0,
|
||||||
'amount_requested': 101.0,
|
'amount_requested': 101.0,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'stop_loss': None,
|
|
||||||
'stop_loss_abs': None,
|
'stop_loss_abs': None,
|
||||||
'stop_loss_pct': None,
|
'stop_loss_pct': None,
|
||||||
'stop_loss_ratio': None,
|
'stop_loss_ratio': None,
|
||||||
'stoploss_order_id': None,
|
'stoploss_order_id': None,
|
||||||
'stoploss_last_update': None,
|
'stoploss_last_update': None,
|
||||||
'stoploss_last_update_timestamp': None,
|
'stoploss_last_update_timestamp': None,
|
||||||
'initial_stop_loss': None,
|
|
||||||
'initial_stop_loss_abs': None,
|
'initial_stop_loss_abs': None,
|
||||||
'initial_stop_loss_pct': None,
|
'initial_stop_loss_pct': None,
|
||||||
'initial_stop_loss_ratio': None,
|
'initial_stop_loss_ratio': None,
|
||||||
'close_profit': None,
|
'close_profit': None,
|
||||||
|
'close_profit_pct': None,
|
||||||
'close_profit_abs': None,
|
'close_profit_abs': None,
|
||||||
|
'profit_ratio': None,
|
||||||
|
'profit_pct': None,
|
||||||
|
'profit_abs': None,
|
||||||
'close_rate_requested': None,
|
'close_rate_requested': None,
|
||||||
'fee_close': 0.0025,
|
'fee_close': 0.0025,
|
||||||
'fee_close_cost': None,
|
'fee_close_cost': None,
|
||||||
@ -898,7 +900,6 @@ def test_to_json(default_conf, fee):
|
|||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
'sell_order_status': None,
|
'sell_order_status': None,
|
||||||
'strategy': None,
|
'strategy': None,
|
||||||
'ticker_interval': None,
|
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'bittrex',
|
'exchange': 'bittrex',
|
||||||
}
|
}
|
||||||
@ -1159,49 +1160,3 @@ def test_select_order(fee):
|
|||||||
assert order.ft_order_side == 'stoploss'
|
assert order.ft_order_side == 'stoploss'
|
||||||
order = trades[4].select_order('sell', False)
|
order = trades[4].select_order('sell', False)
|
||||||
assert order is None
|
assert order is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
|
||||||
def test_PairLock(default_conf):
|
|
||||||
# No lock should be present
|
|
||||||
assert len(PairLock.query.all()) == 0
|
|
||||||
|
|
||||||
pair = 'ETH/BTC'
|
|
||||||
assert not PairLock.is_pair_locked(pair)
|
|
||||||
PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
|
||||||
# ETH/BTC locked for 4 minutes
|
|
||||||
assert PairLock.is_pair_locked(pair)
|
|
||||||
|
|
||||||
# XRP/BTC should not be locked now
|
|
||||||
pair = 'XRP/BTC'
|
|
||||||
assert not PairLock.is_pair_locked(pair)
|
|
||||||
# Unlocking a pair that's not locked should not raise an error
|
|
||||||
PairLock.unlock_pair(pair)
|
|
||||||
|
|
||||||
PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
|
||||||
assert PairLock.is_pair_locked(pair)
|
|
||||||
|
|
||||||
# Get both locks from above
|
|
||||||
locks = PairLock.get_pair_locks(None)
|
|
||||||
assert len(locks) == 2
|
|
||||||
|
|
||||||
# Unlock original pair
|
|
||||||
pair = 'ETH/BTC'
|
|
||||||
PairLock.unlock_pair(pair)
|
|
||||||
assert not PairLock.is_pair_locked(pair)
|
|
||||||
|
|
||||||
pair = 'BTC/USDT'
|
|
||||||
# Lock until 14:30
|
|
||||||
lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc)
|
|
||||||
PairLock.lock_pair(pair, lock_time)
|
|
||||||
|
|
||||||
assert not PairLock.is_pair_locked(pair)
|
|
||||||
assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-10))
|
|
||||||
assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-50))
|
|
||||||
|
|
||||||
# Should not be locked after time expired
|
|
||||||
assert not PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=10))
|
|
||||||
|
|
||||||
locks = PairLock.get_pair_locks(pair, lock_time + timedelta(minutes=-2))
|
|
||||||
assert len(locks) == 1
|
|
||||||
assert 'PairLock' in str(locks[0])
|
|
||||||
|
@ -51,9 +51,10 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
|
|||||||
assert "ohlcv" in ret
|
assert "ohlcv" in ret
|
||||||
assert "trades" in ret
|
assert "trades" in ret
|
||||||
assert "pairs" in ret
|
assert "pairs" in ret
|
||||||
|
assert 'timerange' in ret
|
||||||
|
|
||||||
default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
|
default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
|
||||||
ret = init_plotscript(default_conf)
|
ret = init_plotscript(default_conf, 20)
|
||||||
assert "ohlcv" in ret
|
assert "ohlcv" in ret
|
||||||
assert "TRX/BTC" in ret["ohlcv"]
|
assert "TRX/BTC" in ret["ohlcv"]
|
||||||
assert "ADA/BTC" in ret["ohlcv"]
|
assert "ADA/BTC" in ret["ohlcv"]
|
||||||
|
@ -74,6 +74,10 @@ def test_sync_wallet_at_boot(mocker, default_conf):
|
|||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
assert update_mock.call_count == 1
|
assert update_mock.call_count == 1
|
||||||
|
|
||||||
|
assert freqtrade.wallets.get_free('NOCURRENCY') == 0
|
||||||
|
assert freqtrade.wallets.get_used('NOCURRENCY') == 0
|
||||||
|
assert freqtrade.wallets.get_total('NOCURRENCY') == 0
|
||||||
|
|
||||||
|
|
||||||
def test_sync_wallet_missing_data(mocker, default_conf):
|
def test_sync_wallet_missing_data(mocker, default_conf):
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
Loading…
Reference in New Issue
Block a user