Merge upstream
This commit is contained in:
parent
2dbeb12511
commit
3dd03151a5
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
|
||||||
|
@ -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)`.
|
||||||
|
@ -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,
|
||||||
|
@ -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':
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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),
|
||||||
|
@ -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']),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'],
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
||||||
|
@ -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.py
2
setup.py
@ -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',
|
||||||
|
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=[]))
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
75
tests/exchange/test_ccxt_precise.py
Normal file
75
tests/exchange/test_ccxt_precise.py
Normal 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')
|
@ -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",
|
||||||
[
|
[
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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