Merge branch 'develop' into patch-10
This commit is contained in:
commit
7cd0f8a7b1
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -64,6 +64,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 +309,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 +619,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 "
|
||||||
@ -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,
|
def get_rate(self, pair: str, refresh: bool,
|
||||||
side: EntryExit, is_short: bool) -> float:
|
side: EntryExit, is_short: bool) -> float:
|
||||||
"""
|
"""
|
||||||
@ -1470,20 +1500,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()
|
||||||
|
|
||||||
@ -1703,7 +1720,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,
|
||||||
@ -1739,7 +1757,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)
|
||||||
@ -1857,7 +1876,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:
|
||||||
@ -2674,9 +2695,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:
|
||||||
@ -2691,7 +2713,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:
|
||||||
@ -2701,6 +2723,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.
|
||||||
|
@ -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",
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -536,7 +536,8 @@ 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
|
||||||
self.execute_entry(trade.pair, stake_amount, trade=trade, is_short=trade.is_short)
|
self.execute_entry(trade.pair, stake_amount, price=current_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
|
||||||
@ -586,6 +587,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
|
||||||
@ -601,7 +603,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
|
||||||
@ -746,23 +748,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.')
|
||||||
@ -1212,7 +1217,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:
|
||||||
@ -1408,6 +1414,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'
|
||||||
|
|
||||||
@ -1425,7 +1432,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)
|
||||||
|
|
||||||
@ -1442,8 +1449,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
if not strategy_safe_wrapper(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
|
||||||
@ -1472,7 +1479,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),
|
||||||
|
@ -535,6 +535,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:
|
||||||
@ -545,6 +546,15 @@ 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,
|
||||||
@ -552,7 +562,7 @@ class Backtesting:
|
|||||||
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)
|
exit_tag=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:
|
||||||
@ -566,22 +576,12 @@ 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
|
|
||||||
# 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,)
|
|
||||||
):
|
|
||||||
trade.exit_reason = row[EXIT_TAG_IDX]
|
|
||||||
|
|
||||||
self.order_id_counter += 1
|
self.order_id_counter += 1
|
||||||
order = Order(
|
order = Order(
|
||||||
@ -812,11 +812,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
|
||||||
|
@ -153,6 +153,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]):
|
||||||
@ -491,7 +492,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
|
||||||
@ -502,6 +503,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:
|
||||||
@ -516,8 +518,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)
|
||||||
@ -656,7 +657,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()
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -1410,14 +1410,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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 =
|
||||||
|
2
setup.sh
2
setup.sh
@ -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
|
||||||
|
@ -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=[]))
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -939,6 +939,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')
|
||||||
@ -954,6 +955,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)
|
||||||
|
|
||||||
|
|
||||||
@ -1084,6 +1086,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)
|
||||||
@ -1875,7 +1884,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",
|
||||||
@ -1941,7 +1950,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",
|
||||||
@ -1995,7 +2004,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
|
||||||
@ -3342,7 +3351,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():
|
||||||
@ -3424,6 +3433,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",
|
||||||
[
|
[
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user