Merge upstream

This commit is contained in:
மனோஜ்குமார் பழனிச்சாமி 2022-05-18 02:08:25 +05:30
parent 2dbeb12511
commit 3dd03151a5
42 changed files with 567 additions and 162 deletions

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ ubuntu-18.04, ubuntu-20.04 ] os: [ ubuntu-18.04, ubuntu-20.04, ubuntu-22.04 ]
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10"]
steps: steps:
@ -70,7 +70,7 @@ jobs:
if: matrix.python-version == '3.9' if: matrix.python-version == '3.9'
- name: Coveralls - name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.8') if: (runner.os == 'Linux' && matrix.python-version == '3.9')
env: env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories # Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
@ -157,24 +157,9 @@ jobs:
pip install -e . pip install -e .
- name: Tests - name: Tests
if: (runner.os != 'Linux' || matrix.python-version != '3.8')
run: | run: |
pytest --random-order pytest --random-order
- name: Tests (with cov)
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
- name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
run: |
# Allow failure for coveralls
coveralls -v || true
- name: Backtesting - name: Backtesting
run: | run: |
cp config_examples/config_bittrex.example.json config.json cp config_examples/config_bittrex.example.json config.json
@ -273,7 +258,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.9 python-version: "3.10"
- name: pre-commit dependencies - name: pre-commit dependencies
run: | run: |
@ -292,7 +277,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.9 python-version: "3.10"
- name: Documentation build - name: Documentation build
run: | run: |
@ -358,7 +343,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.8 python-version: "3.9"
- name: Extract branch name - name: Extract branch name
shell: bash shell: bash

View File

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

View File

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

View File

@ -320,6 +320,9 @@ A backtesting result will look like that:
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| Rejected Entry signals | 3089 | | Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 | | Entry/Exit Timeouts | 0 / 0 |
| Canceled Trade Entries | 34 |
| Canceled Entry Orders | 123 |
| Replaced Entry Orders | 89 |
| | | | | |
| Min balance | 0.00945123 BTC | | Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC | | Max balance | 0.01846651 BTC |
@ -416,6 +419,9 @@ It contains some useful key metrics about performance of your strategy on backte
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| Rejected Entry signals | 3089 | | Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 | | Entry/Exit Timeouts | 0 / 0 |
| Canceled Trade Entries | 34 |
| Canceled Entry Orders | 123 |
| Replaced Entry Orders | 89 |
| | | | | |
| Min balance | 0.00945123 BTC | | Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC | | Max balance | 0.01846651 BTC |
@ -447,6 +453,9 @@ It contains some useful key metrics about performance of your strategy on backte
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached. - `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used). - `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
- `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`.
- `Canceled Entry Orders`: Number of entry orders that have been canceled by user request via `adjust_entry_price`.
- `Replaced Entry Orders`: Number of entry orders that have been replaced by user request via `adjust_entry_price`.
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started. - `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started.
Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`. Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.

View File

@ -160,17 +160,17 @@ This filter allows freqtrade to ignore pairs until they have been listed for at
Offsets an incoming pairlist by a given `offset` value. Offsets an incoming pairlist by a given `offset` value.
As an example it can be used in conjunction with `VolumeFilter` to remove the top X volume pairs. Or to split As an example it can be used in conjunction with `VolumeFilter` to remove the top X volume pairs. Or to split a larger pairlist on two bot instances.
a larger pairlist on two bot instances.
Example to remove the first 10 pairs from the pairlist: Example to remove the first 10 pairs from the pairlist, and takes the next 20 (taking items 10-30 of the initial list):
```json ```json
"pairlists": [ "pairlists": [
// ... // ...
{ {
"method": "OffsetFilter", "method": "OffsetFilter",
"offset": 10 "offset": 10,
"number_assets": 20
} }
], ],
``` ```

View File

@ -1,5 +1,5 @@
mkdocs==1.3.0 mkdocs==1.3.0
mkdocs-material==8.2.14 mkdocs-material==8.2.15
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.4 pymdown-extensions==9.4
jinja2==3.1.2 jinja2==3.1.2

View File

@ -79,6 +79,12 @@ def start_download_data(args: Dict[str, Any]) -> None:
data_format_trades=config['dataformat_trades'], data_format_trades=config['dataformat_trades'],
) )
else: else:
if not exchange._ft_has.get('ohlcv_has_history', True):
raise OperationalException(
f"Historic klines not available for {exchange.name}. "
"Please use `--dl-trades` instead for this exchange "
"(will unfortunately take a long time)."
)
pairs_not_available = refresh_backtest_ohlcv_data( pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=expanded_pairs, timeframes=config['timeframes'], exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, datadir=config['datadir'], timerange=timerange,

View File

@ -40,7 +40,7 @@ class HDF5DataHandler(IDataHandler):
return [ return [
( (
cls.rebuild_pair_from_filename(match[1]), cls.rebuild_pair_from_filename(match[1]),
match[2], cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3]) CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1] ) for match in _tmp if match and len(match.groups()) > 1]
@ -109,7 +109,11 @@ class HDF5DataHandler(IDataHandler):
) )
if not filename.exists(): if not filename.exists():
return pd.DataFrame(columns=self._columns) # Fallback mode for 1M files
filename = self._pair_data_filename(
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True)
if not filename.exists():
return pd.DataFrame(columns=self._columns)
where = [] where = []
if timerange: if timerange:
if timerange.starttype == 'date': if timerange.starttype == 'date':

View File

@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
class IDataHandler(ABC): class IDataHandler(ABC):
_OHLCV_REGEX = r'^([a-zA-Z_-]+)\-(\d+\S)\-?([a-zA-Z_]*)?(?=\.)' _OHLCV_REGEX = r'^([a-zA-Z_-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)'
def __init__(self, datadir: Path) -> None: def __init__(self, datadir: Path) -> None:
self._datadir = datadir self._datadir = datadir
@ -193,10 +193,14 @@ class IDataHandler(ABC):
datadir: Path, datadir: Path,
pair: str, pair: str,
timeframe: str, timeframe: str,
candle_type: CandleType candle_type: CandleType,
no_timeframe_modify: bool = False
) -> Path: ) -> Path:
pair_s = misc.pair_to_filename(pair) pair_s = misc.pair_to_filename(pair)
candle = "" candle = ""
if not no_timeframe_modify:
timeframe = cls.timeframe_to_file(timeframe)
if candle_type != CandleType.SPOT: if candle_type != CandleType.SPOT:
datadir = datadir.joinpath('futures') datadir = datadir.joinpath('futures')
candle = f"-{candle_type}" candle = f"-{candle_type}"
@ -210,6 +214,18 @@ class IDataHandler(ABC):
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}') filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
return filename return filename
@staticmethod
def timeframe_to_file(timeframe: str):
return timeframe.replace('M', 'Mo')
@staticmethod
def rebuild_timeframe_from_filename(timeframe: str) -> str:
"""
converts timeframe from disk to file
Replaces mo with M (to avoid problems on case-insensitive filesystems)
"""
return re.sub('1mo', '1M', timeframe, flags=re.IGNORECASE)
@staticmethod @staticmethod
def rebuild_pair_from_filename(pair: str) -> str: def rebuild_pair_from_filename(pair: str) -> str:
""" """

View File

@ -41,7 +41,7 @@ class JsonDataHandler(IDataHandler):
return [ return [
( (
cls.rebuild_pair_from_filename(match[1]), cls.rebuild_pair_from_filename(match[1]),
match[2], cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3]) CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1] ) for match in _tmp if match and len(match.groups()) > 1]
@ -103,9 +103,14 @@ class JsonDataHandler(IDataHandler):
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame :return: DataFrame with ohlcv data, or empty DataFrame
""" """
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type) filename = self._pair_data_filename(
self._datadir, pair, timeframe, candle_type=candle_type)
if not filename.exists(): if not filename.exists():
return DataFrame(columns=self._columns) # Fallback mode for 1M files
filename = self._pair_data_filename(
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True)
if not filename.exists():
return DataFrame(columns=self._columns)
try: try:
pairdata = read_json(filename, orient='values') pairdata = read_json(filename, orient='values')
pairdata.columns = self._columns pairdata.columns = self._columns

View File

@ -16,8 +16,7 @@ import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from cachetools import TTLCache from cachetools import TTLCache
from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_precision
decimal_to_precision)
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
@ -64,6 +63,7 @@ class Exchange:
"time_in_force_parameter": "timeInForce", "time_in_force_parameter": "timeInForce",
"ohlcv_params": {}, "ohlcv_params": {},
"ohlcv_candle_limit": 500, "ohlcv_candle_limit": 500,
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
"ohlcv_require_since": False, "ohlcv_require_since": False,
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
@ -308,12 +308,15 @@ class Exchange:
if self.log_responses: if self.log_responses:
logger.info(f"API {endpoint}: {response}") logger.info(f"API {endpoint}: {response}")
def ohlcv_candle_limit(self, timeframe: str) -> int: def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
""" """
Exchange ohlcv candle limit Exchange ohlcv candle limit
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
:param timeframe: Timeframe to check :param timeframe: Timeframe to check
:param candle_type: Candle-type
:param since_ms: Starting timestamp
:return: Candle limit as integer :return: Candle limit as integer
""" """
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get( return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
@ -615,19 +618,28 @@ class Exchange:
Checks if required startup_candles is more than ohlcv_candle_limit(). Checks if required startup_candles is more than ohlcv_candle_limit().
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
""" """
candle_limit = self.ohlcv_candle_limit(timeframe)
candle_limit = self.ohlcv_candle_limit(
timeframe, self._config['candle_type_def'],
int(date_minus_candles(timeframe, startup_candles).timestamp() * 1000)
if timeframe else None)
# Require one more candle - to account for the still open candle. # Require one more candle - to account for the still open candle.
candle_count = startup_candles + 1 candle_count = startup_candles + 1
# Allow 5 calls to the exchange per pair # Allow 5 calls to the exchange per pair
required_candle_call_count = int( required_candle_call_count = int(
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1)) (candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
if self._ft_has['ohlcv_has_history']:
if required_candle_call_count > 5: if required_candle_call_count > 5:
# Only allow 5 calls per pair to somewhat limit the impact # Only allow 5 calls per pair to somewhat limit the impact
raise OperationalException(
f"This strategy requires {startup_candles} candles to start, "
"which is more than 5x "
f"the amount of candles {self.name} provides for {timeframe}.")
elif required_candle_call_count > 1:
raise OperationalException( raise OperationalException(
f"This strategy requires {startup_candles} candles to start, which is more than 5x " f"This strategy requires {startup_candles} candles to start, which is more than "
f"the amount of candles {self.name} provides for {timeframe}.") f"the amount of candles {self.name} provides for {timeframe}.")
if required_candle_call_count > 1: if required_candle_call_count > 1:
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. " logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
f"This can result in slower operations for the bot. Please check " f"This can result in slower operations for the bot. Please check "
@ -691,10 +703,11 @@ class Exchange:
# counting_mode=self.precisionMode, # counting_mode=self.precisionMode,
# )) # ))
if self.precisionMode == TICK_SIZE: if self.precisionMode == TICK_SIZE:
precision = self.markets[pair]['precision']['price'] precision = Precise(str(self.markets[pair]['precision']['price']))
missing = price % precision price_str = Precise(str(price))
if missing != 0: missing = price_str % precision
price = round(price - missing + precision, 10) if not missing == Precise("0"):
price = round(float(str(price_str - missing + precision)), 14)
else: else:
symbol_prec = self.markets[pair]['precision']['price'] symbol_prec = self.markets[pair]['precision']['price']
big_price = price * pow(10, symbol_prec) big_price = price * pow(10, symbol_prec)
@ -1444,6 +1457,23 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> str:
price_side = conf_strategy['price_side']
if price_side in ('same', 'other'):
price_map = {
('entry', 'long', 'same'): 'bid',
('entry', 'long', 'other'): 'ask',
('entry', 'short', 'same'): 'ask',
('entry', 'short', 'other'): 'bid',
('exit', 'long', 'same'): 'ask',
('exit', 'long', 'other'): 'bid',
('exit', 'short', 'same'): 'bid',
('exit', 'short', 'other'): 'ask',
}
price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
return price_side
def get_rate(self, pair: str, refresh: bool, # noqa: max-complexity: 13 def get_rate(self, pair: str, refresh: bool, # noqa: max-complexity: 13
side: EntryExit, is_short: bool, side: EntryExit, is_short: bool,
order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float: order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float:
@ -1471,20 +1501,7 @@ class Exchange:
conf_strategy = self._config.get(strat_name, {}) conf_strategy = self._config.get(strat_name, {})
price_side = conf_strategy['price_side'] price_side = self._get_price_side(side, is_short, conf_strategy)
if price_side in ('same', 'other'):
price_map = {
('entry', 'long', 'same'): 'bid',
('entry', 'long', 'other'): 'ask',
('entry', 'short', 'same'): 'ask',
('entry', 'short', 'other'): 'bid',
('exit', 'long', 'same'): 'ask',
('exit', 'long', 'other'): 'bid',
('exit', 'short', 'same'): 'bid',
('exit', 'short', 'other'): 'ask',
}
price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
price_side_word = price_side.capitalize() price_side_word = price_side.capitalize()
@ -1735,7 +1752,8 @@ class Exchange:
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
""" """
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
timeframe, candle_type, since_ms)
logger.debug( logger.debug(
"one_call: %s msecs (%s)", "one_call: %s msecs (%s)",
one_call, one_call,
@ -1771,7 +1789,8 @@ class Exchange:
if (not since_ms if (not since_ms
and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)): and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)):
# Multiple calls for one pair - to get more history # Multiple calls for one pair - to get more history
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
timeframe, candle_type, since_ms)
move_to = one_call * self.required_candle_call_count move_to = one_call * self.required_candle_call_count
now = timeframe_to_next_date(timeframe) now = timeframe_to_next_date(timeframe)
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000) since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
@ -1889,7 +1908,9 @@ class Exchange:
pair, timeframe, since_ms, s pair, timeframe, since_ms, s
) )
params = deepcopy(self._ft_has.get('ohlcv_params', {})) params = deepcopy(self._ft_has.get('ohlcv_params', {}))
candle_limit = self.ohlcv_candle_limit(timeframe) candle_limit = self.ohlcv_candle_limit(
timeframe, candle_type=candle_type, since_ms=since_ms)
if candle_type != CandleType.SPOT: if candle_type != CandleType.SPOT:
params.update({'price': candle_type}) params.update({'price': candle_type})
if candle_type != CandleType.FUNDING_RATE: if candle_type != CandleType.FUNDING_RATE:
@ -2706,9 +2727,10 @@ def timeframe_to_msecs(timeframe: str) -> int:
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
""" """
Use Timeframe and determine last possible candle. Use Timeframe and determine the candle start date for this date.
Does not round when given a candle start date.
:param timeframe: timeframe in string format (e.g. "5m") :param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to utcnow() :param date: date to use. Defaults to now(utc)
:returns: date of previous candle (with utc timezone) :returns: date of previous candle (with utc timezone)
""" """
if not date: if not date:
@ -2723,7 +2745,7 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
""" """
Use Timeframe and determine next candle. Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m") :param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to utcnow() :param date: date to use. Defaults to now(utc)
:returns: date of next candle (with utc timezone) :returns: date of next candle (with utc timezone)
""" """
if not date: if not date:
@ -2733,6 +2755,23 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def date_minus_candles(
timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime:
"""
subtract X candles from a date.
:param timeframe: timeframe in string format (e.g. "5m")
:param candle_count: Amount of candles to subtract.
:param date: date to use. Defaults to now(utc)
"""
if not date:
date = datetime.now(timezone.utc)
tf_min = timeframe_to_minutes(timeframe)
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
return new_date
def market_is_active(market: Dict) -> bool: def market_is_active(market: Dict) -> bool:
""" """
Return True if the market is active. Return True if the market is active.

View File

@ -23,6 +23,7 @@ class Kraken(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"ohlcv_candle_limit": 720, "ohlcv_candle_limit": 720,
"ohlcv_has_history": False,
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",

View File

@ -1,13 +1,15 @@
import logging import logging
from typing import Dict, List, Tuple from typing import Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.constants import BuySell from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange import date_minus_candles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,7 +22,7 @@ class Okx(Exchange):
""" """
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 100, "ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h", "funding_fee_timeframe": "8h",
} }
@ -37,6 +39,27 @@ class Okx(Exchange):
net_only = True net_only = True
def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
"""
Exchange ohlcv candle limit
OKX has the following behaviour:
* 300 candles for uptodate data
* 100 candles for historic data
* 100 candles for additional candles (not futures or spot).
:param timeframe: Timeframe to check
:param candle_type: Candle-type
:param since_ms: Starting timestamp
:return: Candle limit as integer
"""
if (
candle_type in (CandleType.FUTURES, CandleType.SPOT) and
(not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000))
):
return 300
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
@retrier @retrier
def additional_exchange_init(self) -> None: def additional_exchange_init(self) -> None:
""" """

View File

@ -542,15 +542,7 @@ class FreqtradeBot(LoggingMixin):
if stake_amount is not None and stake_amount > 0.0: if stake_amount is not None and stake_amount > 0.0:
# We should increase our position # We should increase our position
if self.strategy.max_entry_position_adjustment > -1: self.execute_entry(trade.pair, stake_amount, trade=trade, is_short=trade.is_short)
count_of_entries = trade.nr_of_successful_entries
if count_of_entries > self.strategy.max_entry_position_adjustment:
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
return
else:
logger.debug("Max adjustment entries is set to unlimited.")
self.execute_entry(trade.pair, stake_amount, None and current_entry_rate,
trade=trade, is_short=trade.is_short)
if stake_amount is not None and stake_amount < 0.0: if stake_amount is not None and stake_amount < 0.0:
# We should decrease our position # We should decrease our position
@ -608,6 +600,7 @@ class FreqtradeBot(LoggingMixin):
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
enter_tag: Optional[str] = None, enter_tag: Optional[str] = None,
trade: Optional[Trade] = None, trade: Optional[Trade] = None,
order_adjust: bool = False
) -> bool: ) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
@ -623,7 +616,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade_side, enter_tag, trade) pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust)
if not stake_amount: if not stake_amount:
return False return False
@ -768,23 +761,26 @@ class FreqtradeBot(LoggingMixin):
self, pair: str, price: Optional[float], stake_amount: float, self, pair: str, price: Optional[float], stake_amount: float,
trade_side: LongShort, trade_side: LongShort,
entry_tag: Optional[str], entry_tag: Optional[str],
trade: Optional[Trade] trade: Optional[Trade],
order_adjust: bool,
) -> Tuple[float, float, float]: ) -> Tuple[float, float, float]:
if price: if price:
enter_limit_requested = price enter_limit_requested = price
else: else:
# Calculate price # Calculate price
proposed_enter_rate = self.exchange.get_rate( enter_limit_requested = self.exchange.get_rate(
pair, side='entry', is_short=(trade_side == 'short'), refresh=True) pair, side='entry', is_short=(trade_side == 'short'), refresh=True)
if not order_adjust:
# Don't call custom_entry_price in order-adjust scenario
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=enter_limit_requested)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
proposed_rate=proposed_enter_rate, entry_tag=entry_tag, proposed_rate=enter_limit_requested, entry_tag=entry_tag,
side=trade_side, side=trade_side,
) )
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) enter_limit_requested = self.get_valid_price(custom_entry_price, enter_limit_requested)
if not enter_limit_requested: if not enter_limit_requested:
raise PricingError('Could not determine entry price.') raise PricingError('Could not determine entry price.')
@ -1238,7 +1234,8 @@ class FreqtradeBot(LoggingMixin):
stake_amount=(order_obj.remaining * order_obj.price), stake_amount=(order_obj.remaining * order_obj.price),
price=adjusted_entry_price, price=adjusted_entry_price,
trade=trade, trade=trade,
is_short=trade.is_short is_short=trade.is_short,
order_adjust=True,
) )
def cancel_all_open_orders(self) -> None: def cancel_all_open_orders(self) -> None:
@ -1437,6 +1434,7 @@ class FreqtradeBot(LoggingMixin):
open_date=trade.open_date_utc, open_date=trade.open_date_utc,
) )
exit_type = 'exit' exit_type = 'exit'
exit_reason = exit_tag or exit_check.exit_reason
if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
exit_type = 'stoploss' exit_type = 'stoploss'
@ -1454,7 +1452,7 @@ class FreqtradeBot(LoggingMixin):
pair=trade.pair, trade=trade, pair=trade.pair, trade=trade,
current_time=datetime.now(timezone.utc), current_time=datetime.now(timezone.utc),
proposed_rate=proposed_limit_rate, current_profit=current_profit, proposed_rate=proposed_limit_rate, current_profit=current_profit,
exit_tag=exit_check.exit_reason) exit_tag=exit_reason)
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
@ -1472,8 +1470,8 @@ class FreqtradeBot(LoggingMixin):
if not sub_trade_amt and not strategy_safe_wrapper( if not sub_trade_amt and not strategy_safe_wrapper(
self.strategy.confirm_trade_exit, default_retval=True)( self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force, exit_reason=exit_check.exit_reason, time_in_force=time_in_force, exit_reason=exit_reason,
sell_reason=exit_check.exit_reason, # sellreason -> compatibility sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc)): current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of exiting {trade.pair}") logger.info(f"User requested abortion of exiting {trade.pair}")
return False return False
@ -1502,7 +1500,7 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.exit_order_status = '' trade.exit_order_status = ''
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.exit_reason = exit_tag or exit_check.exit_reason trade.exit_reason = exit_reason
# Lock pair for one candle to prevent immediate re-trading # Lock pair for one candle to prevent immediate re-trading
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),

View File

@ -297,6 +297,9 @@ class Backtesting:
self.rejected_trades = 0 self.rejected_trades = 0
self.timedout_entry_orders = 0 self.timedout_entry_orders = 0
self.timedout_exit_orders = 0 self.timedout_exit_orders = 0
self.canceled_trade_entries = 0
self.canceled_entry_orders = 0
self.replaced_entry_orders = 0
self.dataprovider.clear_cache() self.dataprovider.clear_cache()
if enable_protections: if enable_protections:
self._load_protections(self.strategy) self._load_protections(self.strategy)
@ -548,6 +551,7 @@ class Backtesting:
if exit_.exit_flag: if exit_.exit_flag:
trade.close_date = exit_candle_time trade.close_date = exit_candle_time
exit_reason = exit_.exit_reason
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
try: try:
@ -558,13 +562,23 @@ class Backtesting:
current_profit = trade.calc_profit_ratio(close_rate) current_profit = trade.calc_profit_ratio(close_rate)
order_type = self.strategy.order_types['exit'] order_type = self.strategy.order_types['exit']
if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
# Checks and adds an exit tag, after checking that the length of the
# row has the length for an exit tag column
if(
len(row) > EXIT_TAG_IDX
and row[EXIT_TAG_IDX] is not None
and len(row[EXIT_TAG_IDX]) > 0
and exit_.exit_type in (ExitType.EXIT_SIGNAL,)
):
exit_reason = row[EXIT_TAG_IDX]
# Custom exit pricing only for exit-signals # Custom exit pricing only for exit-signals
if order_type == 'limit': if order_type == 'limit':
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price, close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
default_retval=close_rate)( default_retval=close_rate)(
pair=trade.pair, trade=trade, pair=trade.pair, trade=trade,
current_time=exit_candle_time, current_time=exit_candle_time,
proposed_rate=close_rate, current_profit=current_profit) proposed_rate=close_rate, current_profit=current_profit,
exit_tag=exit_.exit_reason)
# We can't place orders lower than current low. # We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately # freqtrade does not support this in live, and the order would fill immediately
if trade.is_short: if trade.is_short:
@ -578,24 +592,36 @@ class Backtesting:
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=close_rate, rate=close_rate,
time_in_force=time_in_force, time_in_force=time_in_force,
sell_reason=exit_.exit_reason, # deprecated sell_reason=exit_reason, # deprecated
exit_reason=exit_.exit_reason, exit_reason=exit_reason,
current_time=exit_candle_time): current_time=exit_candle_time):
return None return None
trade.exit_reason = exit_.exit_reason trade.exit_reason = exit_reason
# Checks and adds an exit tag, after checking that the length of the self.order_id_counter += 1
# row has the length for an exit tag column order = Order(
if( id=self.order_id_counter,
len(row) > EXIT_TAG_IDX ft_trade_id=trade.id,
and row[EXIT_TAG_IDX] is not None order_date=exit_candle_time,
and len(row[EXIT_TAG_IDX]) > 0 order_update_date=exit_candle_time,
and exit_.exit_type in (ExitType.EXIT_SIGNAL,) ft_is_open=True,
): ft_pair=trade.pair,
trade.exit_reason = row[EXIT_TAG_IDX] order_id=str(self.order_id_counter),
symbol=trade.pair,
return self._exit_trade(trade, row, close_rate) ft_order_side=trade.exit_side,
side=trade.exit_side,
order_type=order_type,
status="open",
price=close_rate,
average=close_rate,
amount=trade.amount,
filled=0,
remaining=trade.amount,
cost=trade.amount * close_rate,
)
trade.orders.append(order)
return trade
return None return None
@ -831,11 +857,11 @@ class Backtesting:
remaining=amount, remaining=amount,
cost=stake_amount + trade.fee_open, cost=stake_amount + trade.fee_open,
) )
trade.orders.append(order)
if pos_adjust and self._get_order_filled(order.price, row): if pos_adjust and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade) order.close_bt_order(current_time, trade)
else: else:
trade.open_order_id = str(self.order_id_counter) trade.open_order_id = str(self.order_id_counter)
trade.orders.append(order)
trade.recalc_trade_from_orders() trade.recalc_trade_from_orders()
return trade return trade
@ -903,6 +929,7 @@ class Backtesting:
return True return True
elif self.check_order_replace(trade, order, current_time, row): elif self.check_order_replace(trade, order, current_time, row):
# delete trade due to user request # delete trade due to user request
self.canceled_trade_entries += 1
return True return True
# default maintain trade # default maintain trade
return False return False
@ -952,6 +979,7 @@ class Backtesting:
return False return False
else: else:
del trade.orders[trade.orders.index(order)] del trade.orders[trade.orders.index(order)]
self.canceled_entry_orders += 1
# place new order if result was not None # place new order if result was not None
if requested_rate: if requested_rate:
@ -959,6 +987,7 @@ class Backtesting:
requested_rate=requested_rate, requested_rate=requested_rate,
requested_stake=(order.remaining * order.price), requested_stake=(order.remaining * order.price),
direction='short' if trade.is_short else 'long') direction='short' if trade.is_short else 'long')
self.replaced_entry_orders += 1
else: else:
# assumption: there can't be multiple open entry orders at any given time # assumption: there can't be multiple open entry orders at any given time
return (trade.nr_of_successful_entries == 0) return (trade.nr_of_successful_entries == 0)
@ -1112,6 +1141,9 @@ class Backtesting:
'rejected_signals': self.rejected_trades, 'rejected_signals': self.rejected_trades,
'timedout_entry_orders': self.timedout_entry_orders, 'timedout_entry_orders': self.timedout_entry_orders,
'timedout_exit_orders': self.timedout_exit_orders, 'timedout_exit_orders': self.timedout_exit_orders,
'canceled_trade_entries': self.canceled_trade_entries,
'canceled_entry_orders': self.canceled_entry_orders,
'replaced_entry_orders': self.replaced_entry_orders,
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
} }

View File

@ -468,6 +468,9 @@ def generate_strategy_stats(pairlist: List[str],
'rejected_signals': content['rejected_signals'], 'rejected_signals': content['rejected_signals'],
'timedout_entry_orders': content['timedout_entry_orders'], 'timedout_entry_orders': content['timedout_entry_orders'],
'timedout_exit_orders': content['timedout_exit_orders'], 'timedout_exit_orders': content['timedout_exit_orders'],
'canceled_trade_entries': content['canceled_trade_entries'],
'canceled_entry_orders': content['canceled_entry_orders'],
'replaced_entry_orders': content['replaced_entry_orders'],
'max_open_trades': max_open_trades, 'max_open_trades': max_open_trades,
'max_open_trades_setting': (config['max_open_trades'] 'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
@ -753,6 +756,12 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Drawdown End', strat_results['drawdown_end']), ('Drawdown End', strat_results['drawdown_end']),
]) ])
entry_adjustment_metrics = [
('Canceled Trade Entries', strat_results.get('canceled_trade_entries', 'N/A')),
('Canceled Entry Orders', strat_results.get('canceled_entry_orders', 'N/A')),
('Replaced Entry Orders', strat_results.get('replaced_entry_orders', 'N/A')),
] if strat_results.get('canceled_entry_orders', 0) > 0 else []
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
# command stores these results and newer version of freqtrade must be able to handle old # command stores these results and newer version of freqtrade must be able to handle old
# results with missing new fields. # results with missing new fields.
@ -801,6 +810,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Entry/Exit Timeouts', ('Entry/Exit Timeouts',
f"{strat_results.get('timedout_entry_orders', 'N/A')} / " f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
f"{strat_results.get('timedout_exit_orders', 'N/A')}"), f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
*entry_adjustment_metrics,
('', ''), # Empty line to improve readability ('', ''), # Empty line to improve readability
('Min balance', round_coin_value(strat_results['csum_min'], ('Min balance', round_coin_value(strat_results['csum_min'],

View File

@ -155,6 +155,7 @@ class Order(_DECL_BASE):
and len(trade.select_filled_orders(trade.entry_side)) == 1): and len(trade.select_filled_orders(trade.entry_side)) == 1):
trade.open_rate = self.price trade.open_rate = self.price
trade.recalc_open_trade_value() trade.recalc_open_trade_value()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
@staticmethod @staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]): def update_orders(orders: List['Order'], order: Dict[str, Any]):
@ -495,7 +496,7 @@ class LocalTrade():
self.stoploss_last_update = datetime.utcnow() self.stoploss_last_update = datetime.utcnow()
def adjust_stop_loss(self, current_price: float, stoploss: float, def adjust_stop_loss(self, current_price: float, stoploss: float,
initial: bool = False) -> None: initial: bool = False, refresh: bool = False) -> None:
""" """
This adjusts the stop loss to it's most recently observed setting This adjusts the stop loss to it's most recently observed setting
:param current_price: Current rate the asset is traded :param current_price: Current rate the asset is traded
@ -506,6 +507,7 @@ class LocalTrade():
if initial and not (self.stop_loss is None or self.stop_loss == 0): if initial and not (self.stop_loss is None or self.stop_loss == 0):
# Don't modify if called with initial and nothing to do # Don't modify if called with initial and nothing to do
return return
refresh = True if refresh and self.nr_of_successful_entries == 1 else False
leverage = self.leverage or 1.0 leverage = self.leverage or 1.0
if self.is_short: if self.is_short:
@ -520,8 +522,7 @@ class LocalTrade():
new_loss = max(self.liquidation_price, new_loss) new_loss = max(self.liquidation_price, new_loss)
# no stop loss assigned yet # no stop loss assigned yet
if self.initial_stop_loss_pct is None: if self.initial_stop_loss_pct is None or refresh:
logger.debug(f"{self.pair} - Assigning new stoploss...")
self._set_stop_loss(new_loss, stoploss) self._set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = new_loss self.initial_stop_loss = new_loss
self.initial_stop_loss_pct = -1 * abs(stoploss) self.initial_stop_loss_pct = -1 * abs(stoploss)
@ -704,7 +705,7 @@ class LocalTrade():
def recalc_open_trade_value(self) -> None: def recalc_open_trade_value(self) -> None:
""" """
Recalculate open_trade_value. Recalculate open_trade_value.
Must be called whenever open_rate, fee_open or is_short is changed. Must be called whenever open_rate, fee_open is changed.
""" """
self.open_trade_value = self._calc_open_trade_value() self.open_trade_value = self._calc_open_trade_value()

View File

@ -32,18 +32,19 @@ class AgeFilter(IPairList):
self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
self._max_days_listed = pairlistconfig.get('max_days_listed', None) self._max_days_listed = pairlistconfig.get('max_days_listed', None)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
if self._min_days_listed < 1: if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1") raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
if self._min_days_listed > exchange.ohlcv_candle_limit('1d'): if self._min_days_listed > candle_limit:
raise OperationalException("AgeFilter requires min_days_listed to not exceed " raise OperationalException("AgeFilter requires min_days_listed to not exceed "
"exchange max request size " "exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})") f"({candle_limit})")
if self._max_days_listed and self._max_days_listed <= self._min_days_listed: if self._max_days_listed and self._max_days_listed <= self._min_days_listed:
raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted") raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted")
if self._max_days_listed and self._max_days_listed > exchange.ohlcv_candle_limit('1d'): if self._max_days_listed and self._max_days_listed > candle_limit:
raise OperationalException("AgeFilter requires max_days_listed to not exceed " raise OperationalException("AgeFilter requires max_days_listed to not exceed "
"exchange max request size " "exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})") f"({candle_limit})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -19,6 +19,7 @@ class OffsetFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._offset = pairlistconfig.get('offset', 0) self._offset = pairlistconfig.get('offset', 0)
self._number_pairs = pairlistconfig.get('number_assets', 0)
if self._offset < 0: if self._offset < 0:
raise OperationalException("OffsetFilter requires offset to be >= 0") raise OperationalException("OffsetFilter requires offset to be >= 0")
@ -36,7 +37,9 @@ class OffsetFilter(IPairList):
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
""" """
return f"{self.name} - Offseting pairs by {self._offset}." if self._number_pairs:
return f"{self.name} - Taking {self._number_pairs} Pairs, starting from {self._offset}."
return f"{self.name} - Offsetting pairs by {self._offset}."
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
@ -50,5 +53,9 @@ class OffsetFilter(IPairList):
self.log_once(f"Offset of {self._offset} is larger than " + self.log_once(f"Offset of {self._offset} is larger than " +
f"pair count of {len(pairlist)}", logger.warning) f"pair count of {len(pairlist)}", logger.warning)
pairs = pairlist[self._offset:] pairs = pairlist[self._offset:]
if self._number_pairs:
pairs = pairs[:self._number_pairs]
self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info) self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info)
return pairs return pairs

View File

@ -38,12 +38,12 @@ class VolatilityFilter(IPairList):
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
if self._days < 1: if self._days < 1:
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
if self._days > exchange.ohlcv_candle_limit('1d'): if self._days > candle_limit:
raise OperationalException("VolatilityFilter requires lookback_days to not " raise OperationalException("VolatilityFilter requires lookback_days to not "
"exceed exchange max request size " f"exceed exchange max request size ({candle_limit})")
f"({exchange.ohlcv_candle_limit('1d')})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -84,12 +84,13 @@ class VolumePairList(IPairList):
raise OperationalException( raise OperationalException(
f'key {self._sort_key} not in {SORT_VALUES}') f'key {self._sort_key} not in {SORT_VALUES}')
candle_limit = exchange.ohlcv_candle_limit(
self._lookback_timeframe, self._config['candle_type_def'])
if self._lookback_period < 0: if self._lookback_period < 0:
raise OperationalException("VolumeFilter requires lookback_period to be >= 0") raise OperationalException("VolumeFilter requires lookback_period to be >= 0")
if self._lookback_period > exchange.ohlcv_candle_limit(self._lookback_timeframe): if self._lookback_period > candle_limit:
raise OperationalException("VolumeFilter requires lookback_period to not " raise OperationalException("VolumeFilter requires lookback_period to not "
"exceed exchange max request size " f"exceed exchange max request size ({candle_limit})")
f"({exchange.ohlcv_candle_limit(self._lookback_timeframe)})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -33,12 +33,12 @@ class RangeStabilityFilter(IPairList):
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
if self._days < 1: if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
if self._days > exchange.ohlcv_candle_limit('1d'): if self._days > candle_limit:
raise OperationalException("RangeStabilityFilter requires lookback_days to not " raise OperationalException("RangeStabilityFilter requires lookback_days to not "
"exceed exchange max request size " f"exceed exchange max request size ({candle_limit})")
f"({exchange.ohlcv_candle_limit('1d')})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -1458,14 +1458,14 @@ class Telegram(RPCHandler):
"Optionally takes a rate at which to sell " "Optionally takes a rate at which to sell "
"(only applies to limit orders).` \n") "(only applies to limit orders).` \n")
message = ( message = (
"_BotControl_\n" "_Bot Control_\n"
"------------\n" "------------\n"
"*/start:* `Starts the trader`\n" "*/start:* `Starts the trader`\n"
"*/stop:* Stops the trader\n" "*/stop:* Stops the trader\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, " "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"
"*/fe <trade_id>|all:* `Alias to /forceexit`" "*/fe <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}" f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n" "*/whitelist:* `Show current whitelist` \n"

View File

@ -6,7 +6,7 @@
coveralls==3.3.1 coveralls==3.3.1
flake8==4.0.1 flake8==4.0.1
flake8-tidy-imports==4.7.0 flake8-tidy-imports==4.8.0
mypy==0.950 mypy==0.950
pre-commit==2.19.0 pre-commit==2.19.0
pytest==7.1.2 pytest==7.1.2
@ -16,7 +16,7 @@ pytest-mock==3.7.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.10.1 isort==5.10.1
# For datetime mocking # For datetime mocking
time-machine==2.6.0 time-machine==2.7.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.5.0 nbconvert==6.5.0

View File

@ -3,7 +3,7 @@
# Required for hyperopt # Required for hyperopt
scipy==1.8.0 scipy==1.8.0
scikit-learn==1.0.2 scikit-learn==1.1.0
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.6.0 filelock==3.7.0
progressbar2==4.0.0 progressbar2==4.0.0

View File

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

View File

@ -2,7 +2,7 @@ numpy==1.22.3
pandas==1.4.2 pandas==1.4.2
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.81.81 ccxt==1.82.61
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==37.0.2 cryptography==37.0.2
aiohttp==3.8.1 aiohttp==3.8.1
@ -34,9 +34,9 @@ orjson==3.6.8
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.76.0 fastapi==0.78.0
uvicorn==0.17.6 uvicorn==0.17.6
pyjwt==2.3.0 pyjwt==2.4.0
aiofiles==0.8.0 aiofiles==0.8.0
psutil==5.9.0 psutil==5.9.0

View File

@ -32,7 +32,7 @@ tests_require =
pytest-mock pytest-mock
packages = find: packages = find:
python_requires = >=3.6 python_requires = >=3.8
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =

View File

@ -42,7 +42,7 @@ setup(
], ],
install_requires=[ install_requires=[
# from requirements.txt # from requirements.txt
'ccxt>=1.79.69', 'ccxt>=1.80.67',
'SQLAlchemy', 'SQLAlchemy',
'python-telegram-bot>=13.4', 'python-telegram-bot>=13.4',
'arrow>=0.17.0', 'arrow>=0.17.0',

View File

@ -25,7 +25,7 @@ function check_installed_python() {
exit 2 exit 2
fi fi
for v in 9 10 8 for v in 10 9 8
do do
PYTHON="python3.${v}" PYTHON="python3.${v}"
which $PYTHON which $PYTHON

View File

@ -835,6 +835,23 @@ def test_download_data_trades(mocker, caplog):
start_download_data(pargs) start_download_data(pargs)
def test_download_data_data_invalid(mocker):
patch_exchange(mocker, id="kraken")
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
args = [
"download-data",
"--exchange", "kraken",
"--pairs", "ETH/BTC", "XRP/BTC",
"--days", "20",
]
pargs = get_args(args)
pargs['config'] = None
with pytest.raises(OperationalException, match=r"Historic klines not available for .*"):
start_download_data(pargs)
def test_start_convert_trades(mocker, caplog): def test_start_convert_trades(mocker, caplog):
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
MagicMock(return_value=[])) MagicMock(return_value=[]))

View File

@ -158,21 +158,22 @@ def test_testdata_path(testdatadir) -> None:
assert str(Path('tests') / 'testdata') in str(testdatadir) assert str(Path('tests') / 'testdata') in str(testdatadir)
@pytest.mark.parametrize("pair,expected_result,candle_type", [ @pytest.mark.parametrize("pair,timeframe,expected_result,candle_type", [
("ETH/BTC", 'freqtrade/hello/world/ETH_BTC-5m.json', ""), ("ETH/BTC", "5m", "freqtrade/hello/world/ETH_BTC-5m.json", ""),
("Fabric Token/ETH", 'freqtrade/hello/world/Fabric_Token_ETH-5m.json', ""), ("ETH/USDT", "1M", "freqtrade/hello/world/ETH_USDT-1Mo.json", ""),
("ETHH20", 'freqtrade/hello/world/ETHH20-5m.json', ""), ("Fabric Token/ETH", "5m", "freqtrade/hello/world/Fabric_Token_ETH-5m.json", ""),
(".XBTBON2H", 'freqtrade/hello/world/_XBTBON2H-5m.json', ""), ("ETHH20", "5m", "freqtrade/hello/world/ETHH20-5m.json", ""),
("ETHUSD.d", 'freqtrade/hello/world/ETHUSD_d-5m.json', ""), (".XBTBON2H", "5m", "freqtrade/hello/world/_XBTBON2H-5m.json", ""),
("ACC_OLD/BTC", 'freqtrade/hello/world/ACC_OLD_BTC-5m.json', ""), ("ETHUSD.d", "5m", "freqtrade/hello/world/ETHUSD_d-5m.json", ""),
("ETH/BTC", 'freqtrade/hello/world/futures/ETH_BTC-5m-mark.json', "mark"), ("ACC_OLD/BTC", "5m", "freqtrade/hello/world/ACC_OLD_BTC-5m.json", ""),
("ACC_OLD/BTC", 'freqtrade/hello/world/futures/ACC_OLD_BTC-5m-index.json', "index"), ("ETH/BTC", "5m", "freqtrade/hello/world/futures/ETH_BTC-5m-mark.json", "mark"),
("ACC_OLD/BTC", "5m", "freqtrade/hello/world/futures/ACC_OLD_BTC-5m-index.json", "index"),
]) ])
def test_json_pair_data_filename(pair, expected_result, candle_type): def test_json_pair_data_filename(pair, timeframe, expected_result, candle_type):
fn = JsonDataHandler._pair_data_filename( fn = JsonDataHandler._pair_data_filename(
Path('freqtrade/hello/world'), Path('freqtrade/hello/world'),
pair, pair,
'5m', timeframe,
CandleType.from_string(candle_type) CandleType.from_string(candle_type)
) )
assert isinstance(fn, Path) assert isinstance(fn, Path)
@ -180,7 +181,7 @@ def test_json_pair_data_filename(pair, expected_result, candle_type):
fn = JsonGzDataHandler._pair_data_filename( fn = JsonGzDataHandler._pair_data_filename(
Path('freqtrade/hello/world'), Path('freqtrade/hello/world'),
pair, pair,
'5m', timeframe,
candle_type=CandleType.from_string(candle_type) candle_type=CandleType.from_string(candle_type)
) )
assert isinstance(fn, Path) assert isinstance(fn, Path)

View File

@ -13,6 +13,7 @@ import pytest
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.exchange import timeframe_to_msecs
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_default_conf_usdt from tests.conftest import get_default_conf_usdt
@ -219,7 +220,7 @@ class TestCCXTExchange():
assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit
assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit
def test_fetch_ohlcv(self, exchange): def test_ccxt_fetch_ohlcv(self, exchange):
exchange, exchangename = exchange exchange, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe'] timeframe = EXCHANGES[exchangename]['timeframe']
@ -231,11 +232,44 @@ class TestCCXTExchange():
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
# assert len(exchange.klines(pair_tf)) > 200 # assert len(exchange.klines(pair_tf)) > 200
# Assume 90% uptime ... # Assume 90% uptime ...
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(timeframe) * 0.90 assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(
timeframe, CandleType.SPOT) * 0.90
# Check if last-timeframe is within the last 2 intervals # Check if last-timeframe is within the last 2 intervals
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
def test_ccxt__async_get_candle_history(self, exchange):
exchange, exchangename = exchange
# For some weired reason, this test returns random lengths for bittrex.
if not exchange._ft_has['ohlcv_has_history'] or exchangename == 'bittrex':
return
pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe']
candle_type = CandleType.SPOT
timeframe_ms = timeframe_to_msecs(timeframe)
now = timeframe_to_prev_date(
timeframe, datetime.now(timezone.utc))
for offset in (360, 120, 30, 10, 5, 2):
since = now - timedelta(days=offset)
since_ms = int(since.timestamp() * 1000)
res = exchange.loop.run_until_complete(exchange._async_get_candle_history(
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
candle_type=candle_type
)
)
assert res
assert res[0] == pair
assert res[1] == timeframe
assert res[2] == candle_type
candles = res[3]
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms
assert len(candles) >= min(candle_count, candle_count1)
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
def test_ccxt_fetch_funding_rate_history(self, exchange_futures): def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
if not exchange: if not exchange:

View File

@ -0,0 +1,75 @@
from ccxt import Precise
ws = Precise('-1.123e-6')
ws = Precise('-1.123e-6')
xs = Precise('0.00000002')
ys = Precise('69696900000')
zs = Precise('0')
def test_precise():
assert ys * xs == '1393.938'
assert xs * ys == '1393.938'
assert ys + xs == '69696900000.00000002'
assert xs + ys == '69696900000.00000002'
assert xs - ys == '-69696899999.99999998'
assert ys - xs == '69696899999.99999998'
assert xs / ys == '0'
assert ys / xs == '3484845000000000000'
assert ws * xs == '-0.00000000000002246'
assert xs * ws == '-0.00000000000002246'
assert ws + xs == '-0.000001103'
assert xs + ws == '-0.000001103'
assert xs - ws == '0.000001143'
assert ws - xs == '-0.000001143'
assert xs / ws == '-0.017809439002671415'
assert ws / xs == '-56.15'
assert zs * ws == '0'
assert zs * xs == '0'
assert zs * ys == '0'
assert ws * zs == '0'
assert xs * zs == '0'
assert ys * zs == '0'
assert zs + ws == '-0.000001123'
assert zs + xs == '0.00000002'
assert zs + ys == '69696900000'
assert ws + zs == '-0.000001123'
assert xs + zs == '0.00000002'
assert ys + zs == '69696900000'
assert abs(Precise('-500.1')) == '500.1'
assert abs(Precise('213')) == '213'
assert abs(Precise('-500.1')) == '500.1'
assert -Precise('213') == '-213'
assert Precise('10.1') % Precise('0.5') == '0.1'
assert Precise('5550') % Precise('120') == '30'
assert Precise('-0.0') == Precise('0')
assert Precise('5.534000') == Precise('5.5340')
assert min(Precise('-3.1415'), Precise('-2')) == '-3.1415'
assert max(Precise('3.1415'), Precise('-2')) == '3.1415'
assert Precise('2') > Precise('1.2345')
assert not Precise('-3.1415') > Precise('-2')
assert not Precise('3.1415') > Precise('3.1415')
assert Precise.string_gt('3.14150000000000000000001', '3.1415')
assert Precise('3.1415') >= Precise('3.1415')
assert Precise('3.14150000000000000000001') >= Precise('3.1415')
assert not Precise('3.1415') < Precise('3.1415')
assert Precise('3.1415') <= Precise('3.1415')
assert Precise('3.1415') <= Precise('3.14150000000000000000001')

View File

@ -17,9 +17,9 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOr
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
calculate_backoff, remove_credentials) calculate_backoff, remove_credentials)
from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, from freqtrade.exchange.exchange import (date_minus_candles, market_is_active, timeframe_to_minutes,
timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_seconds) timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re
@ -356,6 +356,7 @@ def test_amount_to_precision(
(234.53, 4, 0.5, 235.0), (234.53, 4, 0.5, 235.0),
(0.891534, 4, 0.0001, 0.8916), (0.891534, 4, 0.0001, 0.8916),
(64968.89, 4, 0.01, 64968.89), (64968.89, 4, 0.01, 64968.89),
(0.000000003483, 4, 1e-12, 0.000000003483),
]) ])
def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected): def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
@ -990,6 +991,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
def test_validate_timeframes_not_in_config(default_conf, mocker): def test_validate_timeframes_not_in_config(default_conf, mocker):
# TODO: this test does not assert ...
del default_conf["timeframe"] del default_conf["timeframe"]
api_mock = MagicMock() api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange') id_mock = PropertyMock(return_value='test_exchange')
@ -1005,6 +1007,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.validate_required_startup_candles')
Exchange(default_conf) Exchange(default_conf)
@ -1135,6 +1138,13 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'): with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'):
Exchange(default_conf) Exchange(default_conf)
# Emulate kraken mode
ex._ft_has['ohlcv_has_history'] = False
with pytest.raises(OperationalException,
match=r'This strategy requires 2500.*, '
r'which is more than the amount.*'):
ex.validate_required_startup_candles(2500, '5m')
def test_exchange_has(default_conf, mocker): def test_exchange_has(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@ -1926,7 +1936,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8 since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
ret = exchange.get_historic_ohlcv( ret = exchange.get_historic_ohlcv(
pair, pair,
"5m", "5m",
@ -1992,7 +2002,7 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name, candle_ty
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8 since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
ret = exchange.get_historic_ohlcv_as_df( ret = exchange.get_historic_ohlcv_as_df(
pair, pair,
"5m", "5m",
@ -2046,7 +2056,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
) )
# Required candles # Required candles
candles = (end_ts - start_ts) / 300_000 candles = (end_ts - start_ts) / 300_000
exp = candles // exchange.ohlcv_candle_limit('5m') + 1 exp = candles // exchange.ohlcv_candle_limit('5m', CandleType.SPOT) + 1
# Depending on the exchange, this should be called between 1 and 6 times. # Depending on the exchange, this should be called between 1 and 6 times.
assert exchange._api_async.fetch_ohlcv.call_count == exp assert exchange._api_async.fetch_ohlcv.call_count == exp
@ -3417,7 +3427,7 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe] expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
# This should only run for bittrex # This should only run for bittrex
assert exchange_name == 'bittrex' assert exchange_name == 'bittrex'
assert exchange.ohlcv_candle_limit(timeframe) == expected assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
def test_timeframe_to_minutes(): def test_timeframe_to_minutes():
@ -3499,6 +3509,17 @@ def test_timeframe_to_next_date():
assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5) assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5)
def test_date_minus_candles():
date = datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)
assert date_minus_candles("5m", 3, date) == date - timedelta(minutes=15)
assert date_minus_candles("5m", 5, date) == date - timedelta(minutes=25)
assert date_minus_candles("1m", 6, date) == date - timedelta(minutes=6)
assert date_minus_candles("1h", 3, date) == date - timedelta(hours=3, minutes=25)
assert date_minus_candles("1h", 3) == timeframe_to_prev_date('1h') - timedelta(hours=3)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result", "market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result",
[ [

View File

@ -1,12 +1,42 @@
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
def test_okx_ohlcv_candle_limit(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='okx')
timeframes = ('1m', '5m', '1h')
start_time = int(datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp() * 1000)
for timeframe in timeframes:
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 300
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 300
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 100
one_call = int((datetime.now(timezone.utc) - timedelta(
minutes=290 * timeframe_to_minutes(timeframe))).timestamp() * 1000)
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 300
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 300
one_call = int((datetime.now(timezone.utc) - timedelta(
minutes=320 * timeframe_to_minutes(timeframe))).timestamp() * 1000)
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 100
def test_get_maintenance_ratio_and_amt_okx( def test_get_maintenance_ratio_and_amt_okx(
default_conf, default_conf,
mocker, mocker,

View File

@ -522,7 +522,7 @@ tc32 = BTContainer(data=[
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[ trades=[
BTrade(exit_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True) BTrade(exit_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True)
] ]
) )
# Test 33: trailing_stop should be triggered by low of next candle, without adjusting stoploss using # Test 33: trailing_stop should be triggered by low of next candle, without adjusting stoploss using
@ -662,7 +662,7 @@ tc41 = BTContainer(data=[
custom_entry_price=4000, custom_entry_price=4000,
trades=[ trades=[
BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True) BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True)
] ]
) )
# Test 42: Custom-entry-price around candle low # Test 42: Custom-entry-price around candle low
@ -762,7 +762,7 @@ tc48 = BTContainer(data=[
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust [2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 1], [3, 5100, 5100, 4650, 4750, 6172, 0, 1],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.087, stop_loss=-0.2, roi={"0": 0.10}, profit_perc=-0.087,
use_exit_signal=True, timeout=1000, use_exit_signal=True, timeout=1000,
custom_entry_price=4200, adjust_entry_price=5200, custom_entry_price=4200, adjust_entry_price=5200,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)]
@ -777,7 +777,7 @@ tc49 = BTContainer(data=[
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Order readjust [2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1], [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.05, stop_loss=-0.2, roi={"0": 0.10}, profit_perc=0.05,
use_exit_signal=True, timeout=1000, use_exit_signal=True, timeout=1000,
custom_entry_price=5300, adjust_entry_price=5000, custom_entry_price=5300, adjust_entry_price=5000,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)]
@ -811,6 +811,35 @@ tc51 = BTContainer(data=[
trades=[] trades=[]
) )
# Test 52: Custom-entry-price below all candles - readjust order - stoploss
tc52 = BTContainer(data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # enter trade (signal on last candle)
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # stoploss hit?
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.03, roi={"0": 0.10}, profit_perc=-0.03,
use_exit_signal=True, timeout=1000,
custom_entry_price=4200, adjust_entry_price=5200,
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=False)]
)
# Test 53: Custom-entry-price short above all candles - readjust order - stoploss
tc53 = BTContainer(data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0],
[1, 5000, 5200, 4951, 5000, 6172, 0, 0, 0, 0], # enter trade (signal on last candle)
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1], # stoploss hit?
[4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]],
stop_loss=-0.03, roi={"0": 0.10}, profit_perc=-0.03,
use_exit_signal=True, timeout=1000,
custom_entry_price=5300, adjust_entry_price=5000,
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=True)]
)
TESTS = [ TESTS = [
tc0, tc0,
tc1, tc1,
@ -864,6 +893,8 @@ TESTS = [
tc49, tc49,
tc50, tc50,
tc51, tc51,
tc52,
tc53,
] ]
@ -933,3 +964,5 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
assert res.close_date == _get_frame_time_from_offset(trade.close_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
assert res.is_short == trade.is_short assert res.is_short == trade.is_short
backtesting.cleanup()
del backtesting

View File

@ -1169,6 +1169,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}) })
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
@ -1281,6 +1284,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}, },
{ {
@ -1290,6 +1296,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
} }
]) ])
@ -1432,6 +1441,9 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}, },
{ {
@ -1441,6 +1453,9 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
} }
]) ])
@ -1535,6 +1550,9 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}, },
{ {
@ -1544,6 +1562,9 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
} }
]) ])
@ -1607,6 +1628,9 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
}) })
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',

View File

@ -368,6 +368,9 @@ def test_hyperopt_format_results(hyperopt):
'rejected_signals': 2, 'rejected_signals': 2,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'backtest_start_time': 1619718665, 'backtest_start_time': 1619718665,
'backtest_end_time': 1619718665, 'backtest_end_time': 1619718665,
} }
@ -438,6 +441,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'final_balance': 1000, 'final_balance': 1000,
} }

View File

@ -87,6 +87,9 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp,
'run_id': '123', 'run_id': '123',
@ -139,6 +142,9 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
'rejected_signals': 20, 'rejected_signals': 20,
'timedout_entry_orders': 0, 'timedout_entry_orders': 0,
'timedout_exit_orders': 0, 'timedout_exit_orders': 0,
'canceled_trade_entries': 0,
'canceled_entry_orders': 0,
'replaced_entry_orders': 0,
'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp,
'run_id': '124', 'run_id': '124',

View File

@ -470,12 +470,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
"BTC", ['ETH/BTC', 'TKN/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC']),
# VolumePairList with no offset = unchanged pairlist # VolumePairList with no offset = unchanged pairlist
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 0}], {"method": "OffsetFilter", "offset": 0, "number_assets": 0}],
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']),
# VolumePairList with offset = 2 # VolumePairList with offset = 2
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 2}], {"method": "OffsetFilter", "offset": 2}],
"USDT", ['ADAHALF/USDT', 'ADADOUBLE/USDT']), "USDT", ['ADAHALF/USDT', 'ADADOUBLE/USDT']),
# VolumePairList with offset and limit
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 1, "number_assets": 2}],
"USDT", ['NANO/USDT', 'ADAHALF/USDT']),
# VolumePairList with higher offset, than total pairlist # VolumePairList with higher offset, than total pairlist
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 100}], {"method": "OffsetFilter", "offset": 100}],
@ -1152,6 +1156,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
"0.01 and above 0.99 over the last days.'}]", "0.01 and above 0.99 over the last days.'}]",
None None
), ),
({"method": "OffsetFilter", "offset": 5, "number_assets": 10},
"[{'OffsetFilter': 'OffsetFilter - Taking 10 Pairs, starting from 5.'}]",
None
),
]) ])
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig,
desc_expected, exception_expected): desc_expected, exception_expected):

View File

@ -372,11 +372,15 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
freqtrade.enter_positions() freqtrade.enter_positions()
assert len(Trade.get_trades().all()) == 1 assert len(Trade.get_trades().all()) == 1
trade = Trade.get_trades().first() trade: Trade = Trade.get_trades().first()
assert len(trade.orders) == 1 assert len(trade.orders) == 1
assert trade.open_order_id is not None assert trade.open_order_id is not None
assert pytest.approx(trade.stake_amount) == 60 assert pytest.approx(trade.stake_amount) == 60
assert trade.open_rate == 1.96 assert trade.open_rate == 1.96
assert trade.stop_loss_pct is None
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.initial_stop_loss_pct is None
# No adjustment # No adjustment
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
@ -392,6 +396,10 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.open_order_id is not None assert trade.open_order_id is not None
# Open rate is not adjusted yet # Open rate is not adjusted yet
assert trade.open_rate == 1.96 assert trade.open_rate == 1.96
assert trade.stop_loss_pct is None
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.initial_stop_loss_pct is None
# Fill order # Fill order
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True) mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
@ -401,6 +409,10 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.open_order_id is None assert trade.open_order_id is None
# Open rate is not adjusted yet # Open rate is not adjusted yet
assert trade.open_rate == 1.99 assert trade.open_rate == 1.99
assert trade.stop_loss_pct == -0.1
assert trade.stop_loss == 1.99 * 0.9
assert trade.initial_stop_loss == 1.99 * 0.9
assert trade.initial_stop_loss_pct == -0.1
# 2nd order - not filling # 2nd order - not filling
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)