Compare commits
128 Commits
stable
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
|
766336bea0 | ||
|
c3647e49ad | ||
|
37ed37dc76 | ||
|
5cb688c112 | ||
|
3e394d0612 | ||
|
c4c2298686 | ||
|
8564dc10b2 | ||
|
3fb892fcb8 | ||
|
9968348324 | ||
|
fa293c54f8 | ||
|
95449ca886 | ||
|
70fa4a53cd | ||
|
467c63ff01 | ||
|
b8a9c200fe | ||
|
7c10af65a1 | ||
|
e2cd23b1d2 | ||
|
0d408d3d43 | ||
|
2309197771 | ||
|
66fe9abce0 | ||
|
200c18f3e4 | ||
|
351b5f6e65 | ||
|
605cc20a21 | ||
|
f73d2a5371 | ||
|
485a074674 | ||
|
865cf5232b | ||
|
95a24c3133 | ||
|
6833059c70 | ||
|
3833dc0b78 | ||
|
e0d3c771db | ||
|
5a18ab0784 | ||
|
1d66f82b1d | ||
|
2e765fe6d1 | ||
|
21ea02bbcf | ||
|
2ea0157197 | ||
|
03352f3b62 | ||
|
26eb4f7fe6 | ||
|
7e1f3aa545 | ||
|
14532e3a56 | ||
|
a449f7c78c | ||
|
8854ef8cba | ||
|
526943f29e | ||
|
df51111c33 | ||
|
dd8900a1c6 | ||
|
f5a5c2d6b9 | ||
|
a102cfdfc9 | ||
|
be72670ca2 | ||
|
cf2cb94f8d | ||
|
fa3a81b022 | ||
|
7ff30c6df8 | ||
|
7751768b2e | ||
|
9c2cdd4fb9 | ||
|
c2c97d9f78 | ||
|
ac817b7808 | ||
|
4d4f4bf23e | ||
|
c083723698 | ||
|
f8d89c46e5 | ||
|
1952e453bb | ||
|
77985fa591 | ||
|
a75d891007 | ||
|
dae3f72be7 | ||
|
f03a99918a | ||
|
fe02f611fb | ||
|
1b10a3a2bf | ||
|
92a060c5b4 | ||
|
096fd1916c | ||
|
fb09a16127 | ||
|
7fed0782d5 | ||
|
30fc24bd8c | ||
|
7e3de178e1 | ||
|
0c9c9fff0e | ||
|
b96f6670e3 | ||
|
6e02743256 | ||
|
2b4fa92d09 | ||
|
be250230b6 | ||
|
5d33ffc015 | ||
|
b48498f27f | ||
|
e582d8bacb | ||
|
ff40ee655b | ||
|
57deaad806 | ||
|
7779b82277 | ||
|
2bd2058afa | ||
|
bf7936b0af | ||
|
8236bbfd48 | ||
|
4dc13ac16a | ||
|
eb5423469a | ||
|
43496d7929 | ||
|
92c70b6b90 | ||
|
77897c7d6b | ||
|
531861573a | ||
|
c9b904eb0e | ||
|
372f1cb37f | ||
|
a3acdd5240 | ||
|
e6a125719e | ||
|
78a1551798 | ||
|
6f79d14c9c | ||
|
28d8722fa7 | ||
|
2715b2ccf0 | ||
|
2ea575cb31 | ||
|
1b31c54162 | ||
|
e289c10b6c | ||
|
26ed1ca07c | ||
|
b1e20bcd1e | ||
|
12a73bc151 | ||
|
19e112f399 | ||
|
cccf4f305b | ||
|
dc7e834911 | ||
|
a630799984 | ||
|
916e1bbc7c | ||
|
631cb44f5c | ||
|
367186cc34 | ||
|
92f34f262e | ||
|
5e13b48648 | ||
|
6dfb1a1d14 | ||
|
f8330800d1 | ||
|
3ec7c72da1 | ||
|
355fde3bca | ||
|
861c577138 | ||
|
e062a74e70 | ||
|
c330c493d5 | ||
|
3cabcabcbd | ||
|
55781e7f10 | ||
|
f1e831a7b8 | ||
|
159090c0e7 | ||
|
0cb28f3d82 | ||
|
d0d0cbe1d1 | ||
|
02078456fc | ||
|
01dfb1cba8 | ||
|
1132fa6093 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -425,7 +425,7 @@ jobs:
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.3
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
@@ -433,7 +433,7 @@ jobs:
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.3
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
|
@@ -13,12 +13,12 @@ repos:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.4
|
||||
- types-cachetools==5.3.0.5
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.28.11.16
|
||||
- types-tabulate==0.9.0.1
|
||||
- types-python-dateutil==2.8.19.10
|
||||
- SQLAlchemy==2.0.7
|
||||
- types-requests==2.28.11.17
|
||||
- types-tabulate==0.9.0.2
|
||||
- types-python-dateutil==2.8.19.12
|
||||
- SQLAlchemy==2.0.9
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10.10-slim-bullseye as base
|
||||
FROM python:3.10.11-slim-bullseye as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
@@ -42,9 +42,9 @@ if [ $? -ne 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
||||
|
||||
# Tag image for upload and next build step
|
||||
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
||||
|
@@ -58,9 +58,9 @@ fi
|
||||
# Tag image for upload and next build step
|
||||
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
||||
|
||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
||||
docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
||||
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
||||
|
||||
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
||||
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
||||
|
@@ -274,19 +274,20 @@ A backtesting result will look like that:
|
||||
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||
========================================================= EXIT REASON STATS ==========================================================
|
||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||
|:-------------------|--------:|------:|-------:|--------:|
|
||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||
| stop_loss | 166 | 0 | 0 | 166 |
|
||||
| exit_signal | 56 | 36 | 0 | 20 |
|
||||
| force_exit | 2 | 0 | 0 | 2 |
|
||||
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||
==================== EXIT REASON STATS ====================
|
||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||
|:-------------------|--------:|------:|-------:|--------:|
|
||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||
| stop_loss | 166 | 0 | 0 | 166 |
|
||||
| exit_signal | 56 | 36 | 0 | 20 |
|
||||
| force_exit | 2 | 0 | 0 | 2 |
|
||||
|
||||
================== SUMMARY METRICS ==================
|
||||
| Metric | Value |
|
||||
|-----------------------------+---------------------|
|
||||
|
@@ -6,8 +6,8 @@ Low level feature engineering is performed in the user strategy within a set of
|
||||
|
||||
| Function | Description |
|
||||
|---------------|-------------|
|
||||
| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||
| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
||||
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
||||
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
||||
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||
|
||||
@@ -182,11 +182,11 @@ In total, the number of features the user of the presented example strat has cre
|
||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||
|
||||
|
||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||
|
||||
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
||||
|
||||
```py
|
||||
```python
|
||||
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
||||
if metadata["tf"] == "1h":
|
||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||
|
@@ -180,7 +180,7 @@ As you begin to modify the strategy and the prediction model, you will quickly r
|
||||
|
||||
# you can use feature values from dataframe
|
||||
# Assumes the shifted RSI indicator has been generated in the strategy.
|
||||
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_"
|
||||
rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_"
|
||||
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
||||
|
||||
# reward agent for entering trades
|
||||
|
@@ -1,6 +1,6 @@
|
||||
markdown==3.3.7
|
||||
mkdocs==1.4.2
|
||||
mkdocs-material==9.1.4
|
||||
mkdocs-material==9.1.6
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==9.10
|
||||
pymdown-extensions==9.11
|
||||
jinja2==3.1.2
|
||||
|
@@ -9,9 +9,6 @@ This same command can also be used to update freqUI, should there be a new relea
|
||||
|
||||
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
||||
|
||||
!!! info "Alpha release"
|
||||
FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose).
|
||||
|
||||
!!! Note "developers"
|
||||
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
||||
|
||||
|
@@ -279,6 +279,7 @@ Return a summary of your profit/loss and performance.
|
||||
> ∙ `33.095 EUR`
|
||||
>
|
||||
> **Total Trade Count:** `138`
|
||||
> **Bot started:** `2022-07-11 18:40:44`
|
||||
> **First Trade opened:** `3 days ago`
|
||||
> **Latest Trade opened:** `2 minutes ago`
|
||||
> **Avg. Duration:** `2:33:45`
|
||||
@@ -292,6 +293,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th
|
||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
|
||||
|
||||
### /forceexit <trade_id>
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2023.3'
|
||||
__version__ = '2023.4.dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
@@ -598,7 +598,7 @@ CONF_SCHEMA = {
|
||||
"model_type": {"type": "string", "default": "PPO"},
|
||||
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
||||
"net_arch": {"type": "array", "default": [128, 128]},
|
||||
"randomize_startinng_position": {"type": "boolean", "default": False},
|
||||
"randomize_starting_position": {"type": "boolean", "default": False},
|
||||
"model_reward_parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@@ -246,14 +246,8 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Compatibility support for older backtest data.
|
||||
"""
|
||||
df['open_date'] = pd.to_datetime(df['open_date'],
|
||||
utc=True,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
df['close_date'] = pd.to_datetime(df['close_date'],
|
||||
utc=True,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
df['open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
||||
df['close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
||||
# Compatibility support for pre short Columns
|
||||
if 'is_short' not in df.columns:
|
||||
df['is_short'] = False
|
||||
|
@@ -34,7 +34,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
||||
cols = DEFAULT_DATAFRAME_COLUMNS
|
||||
df = DataFrame(ohlcv, columns=cols)
|
||||
|
||||
df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True)
|
||||
df['date'] = to_datetime(df['date'], unit='ms', utc=True)
|
||||
|
||||
# Some exchanges return int values for Volume and even for OHLC.
|
||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||
|
@@ -63,10 +63,7 @@ class FeatherDataHandler(IDataHandler):
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||
pairdata['date'] = to_datetime(pairdata['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||
return pairdata
|
||||
|
||||
def ohlcv_append(
|
||||
|
@@ -75,10 +75,7 @@ class JsonDataHandler(IDataHandler):
|
||||
return DataFrame(columns=self._columns)
|
||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||
pairdata['date'] = to_datetime(pairdata['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||
return pairdata
|
||||
|
||||
def ohlcv_append(
|
||||
|
@@ -62,10 +62,7 @@ class ParquetDataHandler(IDataHandler):
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||
pairdata['date'] = to_datetime(pairdata['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||
return pairdata
|
||||
|
||||
def ohlcv_append(
|
||||
|
@@ -8,15 +8,15 @@ from freqtrade.exchange.bitpanda import Bitpanda
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.bybit import Bybit
|
||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||
from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts,
|
||||
amount_to_precision, available_exchanges,
|
||||
ccxt_exchanges, contracts_to_amount,
|
||||
date_minus_candles, is_exchange_known_ccxt,
|
||||
market_is_active, price_to_precision,
|
||||
timeframe_to_minutes, timeframe_to_msecs,
|
||||
timeframe_to_next_date, timeframe_to_prev_date,
|
||||
timeframe_to_seconds, validate_exchange,
|
||||
validate_exchanges)
|
||||
from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||
amount_to_contracts, amount_to_precision,
|
||||
available_exchanges, ccxt_exchanges,
|
||||
contracts_to_amount, date_minus_candles,
|
||||
is_exchange_known_ccxt, market_is_active,
|
||||
price_to_precision, timeframe_to_minutes,
|
||||
timeframe_to_msecs, timeframe_to_next_date,
|
||||
timeframe_to_prev_date, timeframe_to_seconds,
|
||||
validate_exchange, validate_exchanges)
|
||||
from freqtrade.exchange.gate import Gate
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
|
@@ -30,13 +30,14 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
|
||||
RetryableOrderError, TemporaryError)
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
|
||||
retrier_async)
|
||||
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
|
||||
amount_to_contracts, amount_to_precision,
|
||||
contracts_to_amount, date_minus_candles,
|
||||
is_exchange_known_ccxt, market_is_active,
|
||||
price_to_precision, timeframe_to_minutes,
|
||||
timeframe_to_msecs, timeframe_to_next_date,
|
||||
timeframe_to_prev_date, timeframe_to_seconds)
|
||||
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
|
||||
amount_to_contract_precision, amount_to_contracts,
|
||||
amount_to_precision, contracts_to_amount,
|
||||
date_minus_candles, is_exchange_known_ccxt,
|
||||
market_is_active, price_to_precision,
|
||||
timeframe_to_minutes, timeframe_to_msecs,
|
||||
timeframe_to_next_date, timeframe_to_prev_date,
|
||||
timeframe_to_seconds)
|
||||
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
||||
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
||||
safe_value_fallback2)
|
||||
@@ -59,6 +60,7 @@ class Exchange:
|
||||
# or by specifying them in the configuration.
|
||||
_ft_has_default: Dict = {
|
||||
"stoploss_on_exchange": False,
|
||||
"stop_price_param": "stopPrice",
|
||||
"order_time_in_force": ["GTC"],
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
@@ -734,12 +736,14 @@ class Exchange:
|
||||
"""
|
||||
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
||||
|
||||
def price_to_precision(self, pair: str, price: float) -> float:
|
||||
def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
|
||||
"""
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Rounds up
|
||||
Returns the price rounded to the precision the Exchange accepts.
|
||||
The default price_rounding_mode in conf is ROUND.
|
||||
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||
"""
|
||||
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
|
||||
return price_to_precision(price, self.get_precision_price(pair),
|
||||
self.precisionMode, rounding_mode=rounding_mode)
|
||||
|
||||
def price_get_one_pip(self, pair: str, price: float) -> float:
|
||||
"""
|
||||
@@ -762,12 +766,12 @@ class Exchange:
|
||||
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
||||
|
||||
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
||||
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
|
||||
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
|
||||
if max_stake_amount is None:
|
||||
# * Should never be executed
|
||||
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
||||
'never set max_stake_amount to None')
|
||||
return max_stake_amount / leverage
|
||||
return max_stake_amount
|
||||
|
||||
def _get_stake_amount_limit(
|
||||
self,
|
||||
@@ -785,43 +789,41 @@ class Exchange:
|
||||
except KeyError:
|
||||
raise ValueError(f"Can't get market information for symbol {pair}")
|
||||
|
||||
if isMin:
|
||||
# reserve some percent defined in config (5% default) + stoploss
|
||||
margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||
stoploss_reserve = (
|
||||
margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||
)
|
||||
# it should not be more than 50%
|
||||
stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
|
||||
else:
|
||||
margin_reserve = 1.0
|
||||
stoploss_reserve = 1.0
|
||||
|
||||
stake_limits = []
|
||||
limits = market['limits']
|
||||
if (limits['cost'][limit] is not None):
|
||||
stake_limits.append(
|
||||
self._contracts_to_amount(
|
||||
pair,
|
||||
limits['cost'][limit]
|
||||
)
|
||||
self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
|
||||
)
|
||||
|
||||
if (limits['amount'][limit] is not None):
|
||||
stake_limits.append(
|
||||
self._contracts_to_amount(
|
||||
pair,
|
||||
limits['amount'][limit] * price
|
||||
)
|
||||
self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
|
||||
)
|
||||
|
||||
if not stake_limits:
|
||||
return None if isMin else float('inf')
|
||||
|
||||
# reserve some percent defined in config (5% default) + stoploss
|
||||
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||
amount_reserve_percent = (
|
||||
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||
)
|
||||
# it should not be more than 50%
|
||||
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
|
||||
|
||||
# The value returned should satisfy both limits: for amount (base currency) and
|
||||
# for cost (quote, stake currency), so max() is used here.
|
||||
# See also #2575 at github.
|
||||
return self._get_stake_amount_considering_leverage(
|
||||
max(stake_limits) * amount_reserve_percent,
|
||||
max(stake_limits) if isMin else min(stake_limits),
|
||||
leverage or 1.0
|
||||
) if isMin else min(stake_limits)
|
||||
)
|
||||
|
||||
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
||||
"""
|
||||
@@ -1114,11 +1116,11 @@ class Exchange:
|
||||
"""
|
||||
if not self._ft_has.get('stoploss_on_exchange'):
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
price_param = self._ft_has['stop_price_param']
|
||||
return (
|
||||
order.get('stopPrice', None) is None
|
||||
or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['stopPrice'])))
|
||||
order.get(price_param, None) is None
|
||||
or ((side == "sell" and stop_loss > float(order[price_param])) or
|
||||
(side == "buy" and stop_loss < float(order[price_param])))
|
||||
)
|
||||
|
||||
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
||||
@@ -1158,8 +1160,8 @@ class Exchange:
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopPrice': stop_price})
|
||||
# Verify if stopPrice works for your exchange, else configure stop_price_param
|
||||
params.update({self._ft_has['stop_price_param']: stop_price})
|
||||
return params
|
||||
|
||||
@retrier(retries=0)
|
||||
@@ -1185,12 +1187,12 @@ class Exchange:
|
||||
|
||||
user_order_type = order_types.get('stoploss', 'market')
|
||||
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
||||
|
||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||
stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||
limit_rate = None
|
||||
if user_order_type == 'limit':
|
||||
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
||||
limit_rate = self.price_to_precision(pair, limit_rate)
|
||||
limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
|
@@ -2,11 +2,12 @@
|
||||
Exchange support utils
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil
|
||||
from math import ceil, floor
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
||||
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
||||
from freqtrade.util import FtPrecise
|
||||
@@ -219,35 +220,51 @@ def amount_to_contract_precision(
|
||||
return amount
|
||||
|
||||
|
||||
def price_to_precision(price: float, price_precision: Optional[float],
|
||||
precisionMode: Optional[int]) -> float:
|
||||
def price_to_precision(
|
||||
price: float,
|
||||
price_precision: Optional[float],
|
||||
precisionMode: Optional[int],
|
||||
*,
|
||||
rounding_mode: int = ROUND,
|
||||
) -> float:
|
||||
"""
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Returns the price rounded to the precision the Exchange accepts.
|
||||
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
||||
which does not support rounding up
|
||||
which does not support rounding up.
|
||||
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||
|
||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||
align with amount_to_precision().
|
||||
!!! Rounds up
|
||||
:param price: price to convert
|
||||
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||
:param rounding_mode: rounding mode to use. Defaults to ROUND
|
||||
:return: price rounded up to the precision the Exchange accepts
|
||||
|
||||
"""
|
||||
if price_precision is not None and precisionMode is not None:
|
||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
||||
# precision=price_precision,
|
||||
# counting_mode=self.precisionMode,
|
||||
# ))
|
||||
if precisionMode == TICK_SIZE:
|
||||
if rounding_mode == ROUND:
|
||||
ticks = price / price_precision
|
||||
rounded_ticks = round(ticks)
|
||||
return rounded_ticks * price_precision
|
||||
precision = FtPrecise(price_precision)
|
||||
price_str = FtPrecise(price)
|
||||
missing = price_str % precision
|
||||
if not missing == FtPrecise("0"):
|
||||
price = round(float(str(price_str - missing + precision)), 14)
|
||||
else:
|
||||
symbol_prec = price_precision
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
price = ceil(big_price) / pow(10, symbol_prec)
|
||||
return round(float(str(price_str - missing + precision)), 14)
|
||||
return price
|
||||
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
|
||||
ndigits = round(price_precision)
|
||||
if rounding_mode == ROUND:
|
||||
return round(price, ndigits)
|
||||
ticks = price * (10**ndigits)
|
||||
if rounding_mode == ROUND_UP:
|
||||
return ceil(ticks) / (10**ndigits)
|
||||
if rounding_mode == TRUNCATE:
|
||||
return int(ticks) / (10**ndigits)
|
||||
if rounding_mode == ROUND_DOWN:
|
||||
return floor(ticks) / (10**ndigits)
|
||||
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
|
||||
raise ValueError(f"Unknown precisionMode {precisionMode}")
|
||||
return price
|
||||
|
@@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP
|
||||
from freqtrade.exchange.types import Tickers
|
||||
|
||||
|
||||
@@ -109,6 +110,7 @@ class Kraken(Exchange):
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params.update({'reduceOnly': True})
|
||||
|
||||
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||
if order_types.get('stoploss', 'market') == 'limit':
|
||||
ordertype = "stop-loss-limit"
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
@@ -116,11 +118,11 @@ class Kraken(Exchange):
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
params['price2'] = self.price_to_precision(pair, limit_rate)
|
||||
params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||
else:
|
||||
ordertype = "stop-loss"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
|
@@ -28,6 +28,7 @@ class Okx(Exchange):
|
||||
"funding_fee_timeframe": "8h",
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
"stop_price_param": "stopLossPrice",
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"tickers_have_quoteVolume": False,
|
||||
@@ -162,29 +163,12 @@ class Okx(Exchange):
|
||||
return pair_tiers[-1]['maxNotional'] / leverage
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopLossPrice': stop_price})
|
||||
|
||||
params = super()._get_stop_params(side, ordertype, stop_price)
|
||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
||||
params['tdMode'] = self.margin_mode.value
|
||||
params['posSide'] = self._get_posSide(side, True)
|
||||
return params
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
OKX uses non-default stoploss price naming.
|
||||
"""
|
||||
if not self._ft_has.get('stoploss_on_exchange'):
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
return (
|
||||
order.get('stopLossPrice', None) is None
|
||||
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
|
||||
)
|
||||
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
|
@@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
||||
elif action == Actions.Sell.value and not self.can_short:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
trade_type = "exit"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
@@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type})
|
||||
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||
|
||||
if (self._total_profit < self.max_drawdown or
|
||||
self._total_unrealized_profit < self.max_drawdown):
|
||||
|
@@ -52,16 +52,6 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
"""
|
||||
Action: Neutral, position: Long -> Close Long
|
||||
Action: Neutral, position: Short -> Close Short
|
||||
|
||||
Action: Long, position: Neutral -> Open Long
|
||||
Action: Long, position: Short -> Close Short and Open Long
|
||||
|
||||
Action: Short, position: Neutral -> Open Short
|
||||
Action: Short, position: Long -> Close Long and Open Short
|
||||
"""
|
||||
|
||||
if action == Actions.Neutral.value:
|
||||
self._position = Positions.Neutral
|
||||
@@ -69,16 +59,16 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Long_enter.value:
|
||||
self._position = Positions.Long
|
||||
trade_type = "long"
|
||||
trade_type = "enter_long"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Short_enter.value:
|
||||
self._position = Positions.Short
|
||||
trade_type = "short"
|
||||
trade_type = "enter_short"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
trade_type = "exit"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
@@ -86,7 +76,7 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type})
|
||||
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||
|
||||
if (self._total_profit < self.max_drawdown or
|
||||
self._total_unrealized_profit < self.max_drawdown):
|
||||
|
@@ -53,16 +53,6 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
"""
|
||||
Action: Neutral, position: Long -> Close Long
|
||||
Action: Neutral, position: Short -> Close Short
|
||||
|
||||
Action: Long, position: Neutral -> Open Long
|
||||
Action: Long, position: Short -> Close Short and Open Long
|
||||
|
||||
Action: Short, position: Neutral -> Open Short
|
||||
Action: Short, position: Long -> Close Long and Open Short
|
||||
"""
|
||||
|
||||
if action == Actions.Neutral.value:
|
||||
self._position = Positions.Neutral
|
||||
@@ -70,21 +60,21 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Long_enter.value:
|
||||
self._position = Positions.Long
|
||||
trade_type = "long"
|
||||
trade_type = "enter_long"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Short_enter.value:
|
||||
self._position = Positions.Short
|
||||
trade_type = "short"
|
||||
trade_type = "enter_short"
|
||||
self._last_trade_tick = self._current_tick
|
||||
elif action == Actions.Long_exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
trade_type = "exit_long"
|
||||
self._last_trade_tick = None
|
||||
elif action == Actions.Short_exit.value:
|
||||
self._update_total_profit()
|
||||
self._position = Positions.Neutral
|
||||
trade_type = "neutral"
|
||||
trade_type = "exit_short"
|
||||
self._last_trade_tick = None
|
||||
else:
|
||||
print("case not defined")
|
||||
@@ -92,7 +82,7 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
if trade_type is not None:
|
||||
self.trade_history.append(
|
||||
{'price': self.current_price(), 'index': self._current_tick,
|
||||
'type': trade_type})
|
||||
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||
|
||||
if (self._total_profit < self.max_drawdown or
|
||||
self._total_unrealized_profit < self.max_drawdown):
|
||||
|
@@ -1291,7 +1291,7 @@ class FreqaiDataKitchen:
|
||||
|
||||
return dataframe
|
||||
|
||||
def use_strategy_to_populate_indicators(
|
||||
def use_strategy_to_populate_indicators( # noqa: C901
|
||||
self,
|
||||
strategy: IStrategy,
|
||||
corr_dataframes: dict = {},
|
||||
@@ -1362,12 +1362,12 @@ class FreqaiDataKitchen:
|
||||
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
|
||||
corr_dataframes, base_dataframes, True)
|
||||
|
||||
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
|
||||
if self.live:
|
||||
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
|
||||
dataframe = self.remove_special_chars_from_feature_names(dataframe)
|
||||
|
||||
self.get_unique_classes_from_labels(dataframe)
|
||||
|
||||
dataframe = self.remove_special_chars_from_feature_names(dataframe)
|
||||
|
||||
if self.config.get('reduce_df_footprint', False):
|
||||
dataframe = reduce_dataframe_footprint(dataframe)
|
||||
|
||||
|
@@ -306,7 +306,7 @@ class IFreqaiModel(ABC):
|
||||
if check_features:
|
||||
self.dd.load_metadata(dk)
|
||||
dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators(
|
||||
strategy, prediction_dataframe=dataframe.tail(1), pair=metadata["pair"]
|
||||
strategy, prediction_dataframe=dataframe.tail(1), pair=pair
|
||||
)
|
||||
dk.find_features(dataframe_dummy_features)
|
||||
self.check_if_feature_list_matches_strategy(dk)
|
||||
@@ -316,7 +316,7 @@ class IFreqaiModel(ABC):
|
||||
else:
|
||||
if populate_indicators:
|
||||
dataframe = self.dk.use_strategy_to_populate_indicators(
|
||||
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
|
||||
strategy, prediction_dataframe=dataframe, pair=pair
|
||||
)
|
||||
populate_indicators = False
|
||||
|
||||
@@ -332,6 +332,10 @@ class IFreqaiModel(ABC):
|
||||
dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train)
|
||||
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest)
|
||||
|
||||
dataframe_train = dk.remove_special_chars_from_feature_names(dataframe_train)
|
||||
dataframe_backtest = dk.remove_special_chars_from_feature_names(dataframe_backtest)
|
||||
dk.get_unique_classes_from_labels(dataframe_train)
|
||||
|
||||
if not self.model_exists(dk):
|
||||
dk.find_features(dataframe_train)
|
||||
dk.find_labels(dataframe_train)
|
||||
|
@@ -21,10 +21,12 @@ from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode,
|
||||
State, TradingMode)
|
||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, PricingError)
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
||||
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date,
|
||||
timeframe_to_seconds)
|
||||
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.persistence import Order, PairLocks, Trade, init_db
|
||||
from freqtrade.persistence.key_value_store import set_startup_time
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
@@ -181,6 +183,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
performs startup tasks
|
||||
"""
|
||||
migrate_binance_futures_names(self.config)
|
||||
set_startup_time()
|
||||
|
||||
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
||||
# Update older trades with precision and precision mode
|
||||
@@ -853,7 +856,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.info(f"Canceling stoploss on exchange for {trade}")
|
||||
co = self.exchange.cancel_stoploss_order_with_result(
|
||||
trade.stoploss_order_id, trade.pair, trade.amount)
|
||||
trade.update_order(co)
|
||||
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
|
||||
|
||||
# Reset stoploss order id.
|
||||
trade.stoploss_order_id = None
|
||||
except InvalidOrderException:
|
||||
@@ -945,7 +949,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return enter_limit_requested, stake_amount, leverage
|
||||
|
||||
def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None,
|
||||
def _notify_enter(self, trade: Trade, order: Order, order_type: str,
|
||||
fill: bool = False, sub_trade: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a entry order occurred.
|
||||
@@ -1171,7 +1175,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||
|
||||
if stoploss_order:
|
||||
trade.update_order(stoploss_order)
|
||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||
stoploss_order=True)
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
||||
@@ -1235,7 +1240,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation)
|
||||
stoploss_norm = self.exchange.price_to_precision(
|
||||
trade.pair, trade.stoploss_or_liquidation,
|
||||
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP)
|
||||
|
||||
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
||||
# we check if the update is necessary
|
||||
@@ -1478,8 +1485,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
return False
|
||||
|
||||
try:
|
||||
order = self.exchange.cancel_order_with_result(order['id'], trade.pair,
|
||||
trade.amount)
|
||||
order = self.exchange.cancel_order_with_result(
|
||||
order['id'], trade.pair, trade.amount)
|
||||
except InvalidOrderException:
|
||||
logger.exception(
|
||||
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
||||
@@ -1491,17 +1498,18 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Order might be filled above in odd timing issues.
|
||||
if order.get('status') in ('canceled', 'cancelled'):
|
||||
trade.exit_reason = None
|
||||
trade.open_order_id = None
|
||||
else:
|
||||
trade.exit_reason = exit_reason_prev
|
||||
cancelled = True
|
||||
else:
|
||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
trade.exit_reason = None
|
||||
trade.open_order_id = None
|
||||
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
|
||||
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
||||
trade.open_order_id = None
|
||||
trade.close_rate = None
|
||||
trade.close_rate_requested = None
|
||||
|
||||
@@ -1778,11 +1786,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
return False
|
||||
|
||||
# Update trade with order values
|
||||
logger.info(f'Found open order for {trade}')
|
||||
if not stoploss_order:
|
||||
logger.info(f'Found open order for {trade}')
|
||||
try:
|
||||
order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id,
|
||||
trade.pair,
|
||||
stoploss_order)
|
||||
order = action_order or self.exchange.fetch_order_or_stoploss_order(
|
||||
order_id, trade.pair, stoploss_order)
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch order %s: %s', order_id, exception)
|
||||
return False
|
||||
@@ -1847,7 +1855,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
elif send_msg and not trade.open_order_id and not stoploss_order:
|
||||
# Enter fill
|
||||
self._notify_enter(trade, order, fill=True, sub_trade=sub_trade)
|
||||
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
|
||||
|
||||
def handle_protections(self, pair: str, side: LongShort) -> None:
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
|
@@ -23,6 +23,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
|
||||
|
||||
HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
|
||||
|
||||
def hyperopt_serializer(x):
|
||||
if isinstance(x, np.integer):
|
||||
@@ -76,9 +78,18 @@ class HyperoptTools():
|
||||
with filename.open('w') as f:
|
||||
rapidjson.dump(final_params, f, indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
number_mode=HYPER_PARAMS_FILE_FORMAT
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def load_params(filename: Path) -> Dict:
|
||||
"""
|
||||
Load parameters from file
|
||||
"""
|
||||
with filename.open('r') as f:
|
||||
params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT)
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def try_export_params(config: Config, strategy_name: str, params: Dict):
|
||||
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
|
||||
@@ -189,7 +200,7 @@ class HyperoptTools():
|
||||
for s in ['buy', 'sell', 'protection',
|
||||
'roi', 'stoploss', 'trailing', 'max_open_trades']:
|
||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT))
|
||||
|
||||
else:
|
||||
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",
|
||||
|
@@ -865,6 +865,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
if (results.get('results_per_enter_tag') is not None
|
||||
or results.get('results_per_buy_tag') is not None):
|
||||
# results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
|
||||
@@ -884,11 +889,6 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
for period in backtest_breakdown:
|
||||
days_breakdown_stats = generate_periodic_breakdown_stats(
|
||||
trade_list=results['trades'], period=period)
|
||||
@@ -917,11 +917,11 @@ def show_backtest_results(config: Config, backtest_stats: Dict):
|
||||
strategy, results, stake_currency,
|
||||
config.get('backtest_breakdown', []))
|
||||
|
||||
if len(backtest_stats['strategy']) > 1:
|
||||
if len(backtest_stats['strategy']) > 0:
|
||||
# Print Strategy summary table
|
||||
|
||||
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
||||
print(f"{results['backtest_start']} -> {results['backtest_end']} |"
|
||||
print(f"Backtested {results['backtest_start']} -> {results['backtest_end']} |"
|
||||
f" Max open trades : {results['max_open_trades']}")
|
||||
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore
|
||||
from freqtrade.persistence.models import init_db
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade
|
||||
|
179
freqtrade/persistence/key_value_store.py
Normal file
179
freqtrade/persistence/key_value_store.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import ClassVar, Optional, Union
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from freqtrade.persistence.base import ModelBase, SessionType
|
||||
|
||||
|
||||
ValueTypes = Union[str, datetime, float, int]
|
||||
|
||||
|
||||
class ValueTypesEnum(str, Enum):
|
||||
STRING = 'str'
|
||||
DATETIME = 'datetime'
|
||||
FLOAT = 'float'
|
||||
INT = 'int'
|
||||
|
||||
|
||||
class KeyStoreKeys(str, Enum):
|
||||
BOT_START_TIME = 'bot_start_time'
|
||||
STARTUP_TIME = 'startup_time'
|
||||
|
||||
|
||||
class _KeyValueStoreModel(ModelBase):
|
||||
"""
|
||||
Pair Locks database model.
|
||||
"""
|
||||
__tablename__ = 'KeyValueStore'
|
||||
session: ClassVar[SessionType]
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
||||
key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True)
|
||||
|
||||
value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
|
||||
|
||||
string_value: Mapped[Optional[str]]
|
||||
datetime_value: Mapped[Optional[datetime]]
|
||||
float_value: Mapped[Optional[float]]
|
||||
int_value: Mapped[Optional[int]]
|
||||
|
||||
|
||||
class KeyValueStore():
|
||||
"""
|
||||
Generic bot-wide, persistent key-value store
|
||||
Can be used to store generic values, e.g. very first bot startup time.
|
||||
Supports the types str, datetime, float and int.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def store_value(key: KeyStoreKeys, value: ValueTypes) -> None:
|
||||
"""
|
||||
Store the given value for the given key.
|
||||
:param key: Key to store the value for - can be used in get-value to retrieve the key
|
||||
:param value: Value to store - can be str, datetime, float or int
|
||||
"""
|
||||
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
|
||||
_KeyValueStoreModel.key == key).first()
|
||||
if kv is None:
|
||||
kv = _KeyValueStoreModel(key=key)
|
||||
if isinstance(value, str):
|
||||
kv.value_type = ValueTypesEnum.STRING
|
||||
kv.string_value = value
|
||||
elif isinstance(value, datetime):
|
||||
kv.value_type = ValueTypesEnum.DATETIME
|
||||
kv.datetime_value = value
|
||||
elif isinstance(value, float):
|
||||
kv.value_type = ValueTypesEnum.FLOAT
|
||||
kv.float_value = value
|
||||
elif isinstance(value, int):
|
||||
kv.value_type = ValueTypesEnum.INT
|
||||
kv.int_value = value
|
||||
else:
|
||||
raise ValueError(f'Unknown value type {kv.value_type}')
|
||||
_KeyValueStoreModel.session.add(kv)
|
||||
_KeyValueStoreModel.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def delete_value(key: KeyStoreKeys) -> None:
|
||||
"""
|
||||
Delete the value for the given key.
|
||||
:param key: Key to delete the value for
|
||||
"""
|
||||
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
|
||||
_KeyValueStoreModel.key == key).first()
|
||||
if kv is not None:
|
||||
_KeyValueStoreModel.session.delete(kv)
|
||||
_KeyValueStoreModel.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]:
|
||||
"""
|
||||
Get the value for the given key.
|
||||
:param key: Key to get the value for
|
||||
"""
|
||||
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
|
||||
_KeyValueStoreModel.key == key).first()
|
||||
if kv is None:
|
||||
return None
|
||||
if kv.value_type == ValueTypesEnum.STRING:
|
||||
return kv.string_value
|
||||
if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None:
|
||||
return kv.datetime_value.replace(tzinfo=timezone.utc)
|
||||
if kv.value_type == ValueTypesEnum.FLOAT:
|
||||
return kv.float_value
|
||||
if kv.value_type == ValueTypesEnum.INT:
|
||||
return kv.int_value
|
||||
# This should never happen unless someone messed with the database manually
|
||||
raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover
|
||||
|
||||
@staticmethod
|
||||
def get_string_value(key: KeyStoreKeys) -> Optional[str]:
|
||||
"""
|
||||
Get the value for the given key.
|
||||
:param key: Key to get the value for
|
||||
"""
|
||||
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
|
||||
_KeyValueStoreModel.key == key,
|
||||
_KeyValueStoreModel.value_type == ValueTypesEnum.STRING).first()
|
||||
if kv is None:
|
||||
return None
|
||||
return kv.string_value
|
||||
|
||||
@staticmethod
|
||||
def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]:
|
||||
"""
|
||||
Get the value for the given key.
|
||||
:param key: Key to get the value for
|
||||
"""
|
||||
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
|
||||
_KeyValueStoreModel.key == key,
|
||||
_KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME).first()
|
||||
if kv is None or kv.datetime_value is None:
|
||||
return None
|
||||
return kv.datetime_value.replace(tzinfo=timezone.utc)
|
||||
|
||||
@staticmethod
|
||||
def get_float_value(key: KeyStoreKeys) -> Optional[float]:
|
||||
"""
|
||||
Get the value for the given key.
|
||||
:param key: Key to get the value for
|
||||
"""
|
||||
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
|
||||
_KeyValueStoreModel.key == key,
|
||||
_KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT).first()
|
||||
if kv is None:
|
||||
return None
|
||||
return kv.float_value
|
||||
|
||||
@staticmethod
|
||||
def get_int_value(key: KeyStoreKeys) -> Optional[int]:
|
||||
"""
|
||||
Get the value for the given key.
|
||||
:param key: Key to get the value for
|
||||
"""
|
||||
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
|
||||
_KeyValueStoreModel.key == key,
|
||||
_KeyValueStoreModel.value_type == ValueTypesEnum.INT).first()
|
||||
if kv is None:
|
||||
return None
|
||||
return kv.int_value
|
||||
|
||||
|
||||
def set_startup_time():
|
||||
"""
|
||||
sets bot_start_time to the first trade open date - or "now" on new databases.
|
||||
sets startup_time to "now"
|
||||
"""
|
||||
st = KeyValueStore.get_value('bot_start_time')
|
||||
if st is None:
|
||||
from freqtrade.persistence import Trade
|
||||
t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first()
|
||||
if t is not None:
|
||||
KeyValueStore.store_value('bot_start_time', t.open_date_utc)
|
||||
else:
|
||||
KeyValueStore.store_value('bot_start_time', datetime.now(timezone.utc))
|
||||
KeyValueStore.store_value('startup_time', datetime.now(timezone.utc))
|
@@ -13,6 +13,7 @@ from sqlalchemy.pool import StaticPool
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.persistence.base import ModelBase
|
||||
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
|
||||
from freqtrade.persistence.migrations import check_migrate
|
||||
from freqtrade.persistence.pairlock import PairLock
|
||||
from freqtrade.persistence.trade_model import Order, Trade
|
||||
@@ -76,6 +77,7 @@ def init_db(db_url: str) -> None:
|
||||
bind=engine, autoflush=False), scopefunc=get_request_or_thread_id)
|
||||
Order.session = Trade.session
|
||||
PairLock.session = Trade.session
|
||||
_KeyValueStoreModel.session = Trade.session
|
||||
|
||||
previous_tables = inspect(engine).get_table_names()
|
||||
ModelBase.metadata.create_all(engine)
|
||||
|
@@ -15,7 +15,8 @@ from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPE
|
||||
BuySell, LongShort)
|
||||
from freqtrade.enums import ExitType, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exchange import amount_to_contract_precision, price_to_precision
|
||||
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||
price_to_precision)
|
||||
from freqtrade.leverage import interest
|
||||
from freqtrade.persistence.base import ModelBase, SessionType
|
||||
from freqtrade.util import FtPrecise
|
||||
@@ -597,7 +598,8 @@ class LocalTrade():
|
||||
"""
|
||||
Method used internally to set self.stop_loss.
|
||||
"""
|
||||
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode)
|
||||
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode,
|
||||
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
||||
if not self.stop_loss:
|
||||
self.initial_stop_loss = stop_loss_norm
|
||||
self.stop_loss = stop_loss_norm
|
||||
@@ -628,7 +630,8 @@ class LocalTrade():
|
||||
if self.initial_stop_loss_pct is None or refresh:
|
||||
self.__set_stop_loss(new_loss, stoploss)
|
||||
self.initial_stop_loss = price_to_precision(
|
||||
new_loss, self.price_precision, self.precision_mode)
|
||||
new_loss, self.price_precision, self.precision_mode,
|
||||
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
||||
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
||||
|
||||
# evaluate if the stop loss needs to be updated
|
||||
@@ -692,21 +695,24 @@ class LocalTrade():
|
||||
else:
|
||||
logger.warning(
|
||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||
|
||||
elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
if self.is_open:
|
||||
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
||||
else:
|
||||
raise ValueError(f'Unknown order type: {order.order_type}')
|
||||
|
||||
if order.ft_order_side != self.entry_side:
|
||||
amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
|
||||
self.precision_mode, self.contract_size)
|
||||
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
|
||||
self.close(order.safe_price)
|
||||
else:
|
||||
self.recalc_trade_from_orders()
|
||||
elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'):
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
if self.is_open:
|
||||
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
||||
self.close(order.safe_price)
|
||||
else:
|
||||
raise ValueError(f'Unknown order type: {order.order_type}')
|
||||
|
||||
Trade.commit()
|
||||
|
||||
def close(self, rate: float, *, show_msg: bool = True) -> None:
|
||||
|
@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import ROUND_UP
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
|
||||
@@ -61,9 +62,10 @@ class PrecisionFilter(IPairList):
|
||||
stop_price = ticker['last'] * self._stoploss
|
||||
|
||||
# Adjust stop-prices to precision
|
||||
sp = self._exchange.price_to_precision(pair, stop_price)
|
||||
sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP)
|
||||
|
||||
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99)
|
||||
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99,
|
||||
rounding_mode=ROUND_UP)
|
||||
logger.debug(f"{pair} - {sp} : {stop_gap_price}")
|
||||
|
||||
if sp <= stop_gap_price:
|
||||
|
@@ -108,6 +108,8 @@ class Profit(BaseModel):
|
||||
max_drawdown: float
|
||||
max_drawdown_abs: float
|
||||
trading_volume: Optional[float]
|
||||
bot_start_timestamp: int
|
||||
bot_start_date: str
|
||||
|
||||
|
||||
class SellReason(BaseModel):
|
||||
|
@@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server):
|
||||
|
||||
@contextlib.contextmanager
|
||||
def run_in_thread(self):
|
||||
self.thread = threading.Thread(target=self.run)
|
||||
self.thread = threading.Thread(target=self.run, name='FTUvicorn')
|
||||
self.thread.start()
|
||||
while not self.started:
|
||||
time.sleep(1e-3)
|
||||
|
@@ -26,7 +26,7 @@ from freqtrade.exceptions import ExchangeError, PricingError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.loggers import bufferHandler
|
||||
from freqtrade.misc import decimals_per_coin, shorten_date
|
||||
from freqtrade.persistence import Order, PairLocks, Trade
|
||||
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
@@ -543,6 +543,7 @@ class RPC:
|
||||
first_date = trades[0].open_date if trades else None
|
||||
last_date = trades[-1].open_date if trades else None
|
||||
num = float(len(durations) or 1)
|
||||
bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)
|
||||
return {
|
||||
'profit_closed_coin': profit_closed_coin_sum,
|
||||
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
|
||||
@@ -576,6 +577,8 @@ class RPC:
|
||||
'max_drawdown': max_drawdown,
|
||||
'max_drawdown_abs': max_drawdown_abs,
|
||||
'trading_volume': trading_volume,
|
||||
'bot_start_timestamp': int(bot_start.timestamp() * 1000) if bot_start else 0,
|
||||
'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '',
|
||||
}
|
||||
|
||||
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
||||
@@ -1193,6 +1196,7 @@ class RPC:
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
|
||||
strategy.ft_bot_start()
|
||||
|
||||
df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})
|
||||
|
||||
|
@@ -52,7 +52,7 @@ class __RPCBuyMsgBase(RPCSendMsgBase):
|
||||
direction: str
|
||||
limit: float
|
||||
open_rate: float
|
||||
order_type: Optional[str] # TODO: why optional??
|
||||
order_type: str
|
||||
stake_amount: float
|
||||
stake_currency: str
|
||||
fiat_currency: Optional[str]
|
||||
|
@@ -819,7 +819,7 @@ class Telegram(RPCHandler):
|
||||
best_pair = stats['best_pair']
|
||||
best_pair_profit_ratio = stats['best_pair_profit_ratio']
|
||||
if stats['trade_count'] == 0:
|
||||
markdown_msg = 'No trades yet.'
|
||||
markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
|
||||
else:
|
||||
# Message to display
|
||||
if stats['closed_trade_count'] > 0:
|
||||
@@ -838,6 +838,7 @@ class Telegram(RPCHandler):
|
||||
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
||||
f"*Total Trade Count:* `{trade_count}`\n"
|
||||
f"*Bot started:* `{stats['bot_start_date']}`\n"
|
||||
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
|
||||
f"`{first_trade_date}`\n"
|
||||
f"*Latest Trade opened:* `{latest_trade_date}`\n"
|
||||
|
@@ -8,7 +8,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
from freqtrade.strategy.parameters import BaseParameter
|
||||
|
||||
@@ -124,8 +124,7 @@ class HyperStrategyMixin:
|
||||
if filename.is_file():
|
||||
logger.info(f"Loading parameters from file {filename}")
|
||||
try:
|
||||
with filename.open('r') as f:
|
||||
params = json_load(f)
|
||||
params = HyperoptTools.load_params(filename)
|
||||
if params.get('strategy_name') != self.__class__.__name__:
|
||||
raise OperationalException('Invalid parameter file provided.')
|
||||
return params
|
||||
|
@@ -7,10 +7,10 @@
|
||||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==3.3.1
|
||||
ruff==0.0.259
|
||||
mypy==1.1.1
|
||||
pre-commit==3.2.1
|
||||
pytest==7.2.2
|
||||
ruff==0.0.261
|
||||
mypy==1.2.0
|
||||
pre-commit==3.2.2
|
||||
pytest==7.3.0
|
||||
pytest-asyncio==0.21.0
|
||||
pytest-cov==4.0.0
|
||||
pytest-mock==3.10.0
|
||||
@@ -22,11 +22,11 @@ time-machine==2.9.0
|
||||
httpx==0.23.3
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
nbconvert==7.2.10
|
||||
nbconvert==7.3.1
|
||||
|
||||
# mypy types
|
||||
types-cachetools==5.3.0.4
|
||||
types-cachetools==5.3.0.5
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.28.11.16
|
||||
types-tabulate==0.9.0.1
|
||||
types-python-dateutil==2.8.19.10
|
||||
types-requests==2.28.11.17
|
||||
types-tabulate==0.9.0.2
|
||||
types-python-dateutil==2.8.19.12
|
||||
|
@@ -7,5 +7,5 @@ scikit-learn==1.1.3
|
||||
joblib==1.2.0
|
||||
catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11'
|
||||
lightgbm==3.3.5
|
||||
xgboost==1.7.4
|
||||
tensorboard==2.12.0
|
||||
xgboost==1.7.5
|
||||
tensorboard==2.12.1
|
||||
|
@@ -5,5 +5,5 @@
|
||||
scipy==1.10.1
|
||||
scikit-learn==1.1.3
|
||||
scikit-optimize==0.9.0
|
||||
filelock==3.10.6
|
||||
filelock==3.11.0
|
||||
progressbar2==4.2.0
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.13.1
|
||||
plotly==5.14.1
|
||||
|
@@ -2,11 +2,11 @@ numpy==1.24.2
|
||||
pandas==1.5.3
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==3.0.37
|
||||
ccxt==3.0.59
|
||||
cryptography==40.0.1
|
||||
aiohttp==3.8.4
|
||||
SQLAlchemy==2.0.7
|
||||
python-telegram-bot==13.15
|
||||
SQLAlchemy==2.0.9
|
||||
python-telegram-bot==20.2
|
||||
arrow==1.2.3
|
||||
cachetools==4.2.2
|
||||
requests==2.28.2
|
||||
@@ -28,7 +28,7 @@ py_find_1st==1.1.5
|
||||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.10
|
||||
# Properly format api responses
|
||||
orjson==3.8.8
|
||||
orjson==3.8.10
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
@@ -50,10 +50,10 @@ prompt-toolkit==3.0.38
|
||||
python-dateutil==2.8.2
|
||||
|
||||
#Futures
|
||||
schedule==1.1.0
|
||||
schedule==1.2.0
|
||||
|
||||
#WS Messages
|
||||
websockets==10.4
|
||||
websockets==11.0.1
|
||||
janus==1.0.0
|
||||
|
||||
ast-comments==1.0.1
|
||||
|
2
setup.py
2
setup.py
@@ -59,7 +59,7 @@ setup(
|
||||
install_requires=[
|
||||
# from requirements.txt
|
||||
'ccxt>=2.6.26',
|
||||
'SQLAlchemy',
|
||||
'SQLAlchemy>=2.0.6',
|
||||
'python-telegram-bot>=13.4',
|
||||
'arrow>=0.17.0',
|
||||
'cachetools',
|
||||
|
@@ -48,7 +48,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte
|
||||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||
default_conf['trading_mode'] = trademode
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
@@ -127,7 +127,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
order_type = 'stop_loss_limit'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
|
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
import arrow
|
||||
import ccxt
|
||||
import pytest
|
||||
from ccxt import DECIMAL_PLACES, ROUND, ROUND_UP, TICK_SIZE, TRUNCATE
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
@@ -315,35 +316,54 @@ def test_amount_to_precision(amount, precision_mode, precision, expected,):
|
||||
assert amount_to_precision(amount, precision, precision_mode) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
||||
(2.34559, 2, 4, 2.3456),
|
||||
(2.34559, 2, 5, 2.34559),
|
||||
(2.34559, 2, 3, 2.346),
|
||||
(2.9999, 2, 3, 3.000),
|
||||
(2.9909, 2, 3, 2.991),
|
||||
# Tests for Tick_size
|
||||
(2.34559, 4, 0.0001, 2.3456),
|
||||
(2.34559, 4, 0.00001, 2.34559),
|
||||
(2.34559, 4, 0.001, 2.346),
|
||||
(2.9999, 4, 0.001, 3.000),
|
||||
(2.9909, 4, 0.001, 2.991),
|
||||
(2.9909, 4, 0.005, 2.995),
|
||||
(2.9973, 4, 0.005, 3.0),
|
||||
(2.9977, 4, 0.005, 3.0),
|
||||
(234.43, 4, 0.5, 234.5),
|
||||
(234.53, 4, 0.5, 235.0),
|
||||
(0.891534, 4, 0.0001, 0.8916),
|
||||
(64968.89, 4, 0.01, 64968.89),
|
||||
(0.000000003483, 4, 1e-12, 0.000000003483),
|
||||
|
||||
@pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [
|
||||
# Tests for DECIMAL_PLACES, ROUND_UP
|
||||
(2.34559, 2, 4, 2.3456, ROUND_UP),
|
||||
(2.34559, 2, 5, 2.34559, ROUND_UP),
|
||||
(2.34559, 2, 3, 2.346, ROUND_UP),
|
||||
(2.9999, 2, 3, 3.000, ROUND_UP),
|
||||
(2.9909, 2, 3, 2.991, ROUND_UP),
|
||||
# Tests for DECIMAL_PLACES, ROUND
|
||||
(2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND),
|
||||
(2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND),
|
||||
(2.49, DECIMAL_PLACES, 0, 2., ROUND),
|
||||
(2.51, DECIMAL_PLACES, 0, 3., ROUND),
|
||||
(5.1, DECIMAL_PLACES, -1, 10., ROUND),
|
||||
(4.9, DECIMAL_PLACES, -1, 0., ROUND),
|
||||
# Tests for TICK_SIZE, ROUND_UP
|
||||
(2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP),
|
||||
(2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP),
|
||||
(2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP),
|
||||
(2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP),
|
||||
(2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP),
|
||||
(2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP),
|
||||
(2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP),
|
||||
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP),
|
||||
(234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP),
|
||||
(234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP),
|
||||
(0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP),
|
||||
(64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP),
|
||||
(0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP),
|
||||
# Tests for TICK_SIZE, ROUND
|
||||
(2.49, TICK_SIZE, 1., 2., ROUND),
|
||||
(2.51, TICK_SIZE, 1., 3., ROUND),
|
||||
(2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND),
|
||||
(2.000000049, TICK_SIZE, 0.0000001, 2., ROUND),
|
||||
(2.9909, TICK_SIZE, 0.005, 2.990, ROUND),
|
||||
(2.9973, TICK_SIZE, 0.005, 2.995, ROUND),
|
||||
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND),
|
||||
(234.24, TICK_SIZE, 0.5, 234., ROUND),
|
||||
(234.26, TICK_SIZE, 0.5, 234.5, ROUND),
|
||||
# Tests for TRUNCATTE
|
||||
(2.34559, 2, 4, 2.3455, TRUNCATE),
|
||||
(2.34559, 2, 5, 2.34559, TRUNCATE),
|
||||
(2.34559, 2, 3, 2.345, TRUNCATE),
|
||||
(2.9999, 2, 3, 2.999, TRUNCATE),
|
||||
(2.9909, 2, 3, 2.990, TRUNCATE),
|
||||
])
|
||||
def test_price_to_precision(price, precision_mode, precision, expected):
|
||||
# digits counting mode
|
||||
# DECIMAL_PLACES = 2
|
||||
# SIGNIFICANT_DIGITS = 3
|
||||
# TICK_SIZE = 4
|
||||
|
||||
assert price_to_precision(price, precision, precision_mode) == expected
|
||||
def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode):
|
||||
assert price_to_precision(
|
||||
price, precision, precision_mode, rounding_mode=rounding_mode) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
||||
@@ -417,7 +437,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
|
||||
}
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
||||
expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss))
|
||||
expected_result = 2 * 2 * (1 + 0.05)
|
||||
assert pytest.approx(result) == expected_result
|
||||
# With Leverage
|
||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
|
||||
@@ -426,14 +446,14 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
|
||||
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
|
||||
assert result == 20000
|
||||
|
||||
# min amount and cost are set (cost is minimal)
|
||||
# min amount and cost are set (cost is minimal and therefore ignored)
|
||||
markets["ETH/BTC"]["limits"] = {
|
||||
'cost': {'min': 2, 'max': None},
|
||||
'amount': {'min': 2, 'max': None},
|
||||
}
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
||||
expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss))
|
||||
expected_result = max(2, 2 * 2) * (1 + 0.05)
|
||||
assert pytest.approx(result) == expected_result
|
||||
# With Leverage
|
||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
|
||||
@@ -476,6 +496,9 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
|
||||
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
|
||||
assert result == 1000
|
||||
|
||||
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2, 12.0)
|
||||
assert result == 1000 / 12
|
||||
|
||||
markets["ETH/BTC"]["contractSize"] = '0.01'
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
@@ -5281,7 +5304,7 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_contract_size = MagicMock(return_value=contract_size)
|
||||
@@ -5301,3 +5324,10 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
|
||||
assert order['cost'] == 100
|
||||
assert order['filled'] == 100
|
||||
assert order['remaining'] == 100
|
||||
|
||||
|
||||
def test_price_to_precision_with_default_conf(default_conf, mocker):
|
||||
conf = copy.deepcopy(default_conf)
|
||||
patched_ex = get_patched_exchange(mocker, conf)
|
||||
prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101)
|
||||
assert prec_price == 1.00000001
|
||||
|
@@ -27,7 +27,7 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected,
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
|
||||
@@ -80,7 +80,7 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker):
|
||||
order_type = 'stop-limit'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
|
||||
|
@@ -29,7 +29,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.create_order(
|
||||
@@ -192,7 +192,7 @@ def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adj
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
@@ -263,7 +263,7 @@ def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side):
|
||||
api_mock = MagicMock()
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
|
||||
|
@@ -27,7 +27,7 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
if order_type == 'limit':
|
||||
@@ -88,7 +88,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
|
||||
order_type = 'market'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
|
||||
|
@@ -119,6 +119,7 @@ def make_unfiltered_dataframe(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
freqai.dk.pair = "ADA/BTC"
|
||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk)
|
||||
@@ -152,6 +153,7 @@ def make_data_dictionary(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
freqai.dk.pair = "ADA/BTC"
|
||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk)
|
||||
|
@@ -19,6 +19,7 @@ def test_update_historic_data(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
@@ -41,6 +42,7 @@ def test_load_all_pairs_histories(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
@@ -60,6 +62,7 @@ def test_get_base_and_corr_dataframes(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
sub_timerange = TimeRange.parse_timerange("20180111-20180114")
|
||||
@@ -87,6 +90,7 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180114")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
sub_timerange = TimeRange.parse_timerange("20180111-20180114")
|
||||
@@ -103,8 +107,9 @@ def test_get_timerange_from_live_historic_predictions(mocker, freqai_conf):
|
||||
exchange = get_patched_exchange(mocker, freqai_conf)
|
||||
strategy.dp = DataProvider(freqai_conf, exchange)
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.live = False
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = False
|
||||
timerange = TimeRange.parse_timerange("20180126-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
sub_timerange = TimeRange.parse_timerange("20180128-20180130")
|
||||
|
@@ -180,6 +180,7 @@ def test_get_full_model_path(mocker, freqai_conf, model):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
|
@@ -87,6 +87,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
|
||||
freqai.live = True
|
||||
freqai.can_short = can_short
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
freqai.dk.set_paths('ADA/BTC', 10000)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
@@ -135,6 +136,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
@@ -178,6 +180,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
@@ -371,6 +374,9 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
|
||||
sub_timerange = TimeRange.parse_timerange("20180129-20180130")
|
||||
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
|
||||
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
|
||||
df = strategy.set_freqai_targets(df.copy(), metadata={"pair": "LTC/BTC"})
|
||||
df = freqai.dk.remove_special_chars_from_feature_names(df)
|
||||
freqai.dk.get_unique_classes_from_labels(df)
|
||||
freqai.dk.pair = "ADA/BTC"
|
||||
freqai.dk.full_df = df.fillna(0)
|
||||
freqai.dk.full_df
|
||||
@@ -394,6 +400,7 @@ def test_principal_component_analysis(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
@@ -425,10 +432,12 @@ def test_plot_feature_importance(mocker, freqai_conf):
|
||||
freqai = strategy.freqai
|
||||
freqai.live = True
|
||||
freqai.dk = FreqaiDataKitchen(freqai_conf)
|
||||
freqai.dk.live = True
|
||||
timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
freqai.dd.pair_dict = MagicMock()
|
||||
freqai.dd.pair_dict = {"ADA/BTC": {"model_filename": "fake_name",
|
||||
"trained_timestamp": 1, "data_path": "", "extras": {}}}
|
||||
|
||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
new_timerange = TimeRange.parse_timerange("20180120-20180130")
|
||||
|
69
tests/persistence/test_key_value_store.py
Normal file
69
tests/persistence/test_key_value_store.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time
|
||||
from tests.conftest import create_mock_trades_usdt
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_key_value_store(time_machine):
|
||||
start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc)
|
||||
time_machine.move_to(start, tick=False)
|
||||
|
||||
KeyValueStore.store_value("test", "testStringValue")
|
||||
KeyValueStore.store_value("test_dt", datetime.now(timezone.utc))
|
||||
KeyValueStore.store_value("test_float", 22.51)
|
||||
KeyValueStore.store_value("test_int", 15)
|
||||
|
||||
assert KeyValueStore.get_value("test") == "testStringValue"
|
||||
assert KeyValueStore.get_value("test") == "testStringValue"
|
||||
assert KeyValueStore.get_string_value("test") == "testStringValue"
|
||||
assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc)
|
||||
assert KeyValueStore.get_datetime_value("test_dt") == datetime.now(timezone.utc)
|
||||
assert KeyValueStore.get_string_value("test_dt") is None
|
||||
assert KeyValueStore.get_float_value("test_dt") is None
|
||||
assert KeyValueStore.get_int_value("test_dt") is None
|
||||
assert KeyValueStore.get_value("test_float") == 22.51
|
||||
assert KeyValueStore.get_float_value("test_float") == 22.51
|
||||
assert KeyValueStore.get_value("test_int") == 15
|
||||
assert KeyValueStore.get_int_value("test_int") == 15
|
||||
assert KeyValueStore.get_datetime_value("test_int") is None
|
||||
|
||||
time_machine.move_to(start + timedelta(days=20, hours=5), tick=False)
|
||||
assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc)
|
||||
assert KeyValueStore.get_value("test_dt") == start
|
||||
# Test update works
|
||||
KeyValueStore.store_value("test_dt", datetime.now(timezone.utc))
|
||||
assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc)
|
||||
|
||||
KeyValueStore.store_value("test_float", 23.51)
|
||||
assert KeyValueStore.get_value("test_float") == 23.51
|
||||
# test deleting
|
||||
KeyValueStore.delete_value("test_float")
|
||||
assert KeyValueStore.get_value("test_float") is None
|
||||
# Delete same value again (should not fail)
|
||||
KeyValueStore.delete_value("test_float")
|
||||
|
||||
with pytest.raises(ValueError, match=r"Unknown value type"):
|
||||
KeyValueStore.store_value("test_float", {'some': 'dict'})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_set_startup_time(fee, time_machine):
|
||||
create_mock_trades_usdt(fee)
|
||||
start = datetime.now(timezone.utc)
|
||||
time_machine.move_to(start, tick=False)
|
||||
set_startup_time()
|
||||
|
||||
assert KeyValueStore.get_value("startup_time") == start
|
||||
initial_time = KeyValueStore.get_value("bot_start_time")
|
||||
assert initial_time <= start
|
||||
|
||||
# Simulate bot restart
|
||||
new_start = start + timedelta(days=5)
|
||||
time_machine.move_to(new_start, tick=False)
|
||||
set_startup_time()
|
||||
|
||||
assert KeyValueStore.get_value("startup_time") == new_start
|
||||
assert KeyValueStore.get_value("bot_start_time") == initial_time
|
@@ -883,6 +883,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
|
||||
'max_drawdown': ANY,
|
||||
'max_drawdown_abs': ANY,
|
||||
'trading_volume': expected['trading_volume'],
|
||||
'bot_start_timestamp': 0,
|
||||
'bot_start_date': '',
|
||||
}
|
||||
|
||||
|
||||
@@ -1403,10 +1405,10 @@ def test_api_pair_candles(botclient, ohlcv_history):
|
||||
])
|
||||
|
||||
|
||||
def test_api_pair_history(botclient, ohlcv_history):
|
||||
def test_api_pair_history(botclient, mocker):
|
||||
ftbot, client = botclient
|
||||
timeframe = '5m'
|
||||
|
||||
lfm = mocker.patch('freqtrade.strategy.interface.IStrategy.load_freqAI_model')
|
||||
# No pair
|
||||
rc = client_get(client,
|
||||
f"{BASE_URI}/pair_history?timeframe={timeframe}"
|
||||
@@ -1440,6 +1442,7 @@ def test_api_pair_history(botclient, ohlcv_history):
|
||||
assert len(rc.json()['data']) == rc.json()['length']
|
||||
assert 'columns' in rc.json()
|
||||
assert 'data' in rc.json()
|
||||
assert lfm.call_count == 1
|
||||
assert rc.json()['pair'] == 'UNITTEST/BTC'
|
||||
assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY
|
||||
assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00'
|
||||
|
@@ -986,7 +986,8 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
||||
}
|
||||
}
|
||||
}
|
||||
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
|
||||
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
|
||||
return_value=expected_result)
|
||||
PairLocks.timeframe = default_conf['timeframe']
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
assert strategy.stoploss == -0.05
|
||||
@@ -1005,11 +1006,13 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
|
||||
}
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
|
||||
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
|
||||
return_value=expected_result)
|
||||
with pytest.raises(OperationalException, match="Invalid parameter file provided."):
|
||||
StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError()))
|
||||
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
|
||||
MagicMock(side_effect=ValueError()))
|
||||
|
||||
StrategyResolver.load_strategy(default_conf)
|
||||
assert log_has("Invalid parameter file format.", caplog)
|
||||
|
@@ -356,7 +356,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
|
||||
(5.0, True, True, 99),
|
||||
(0.049, True, False, 99), # Amount will be adjusted to min - which is 0.051
|
||||
(0.042, True, False, 99), # Amount will be adjusted to min - which is 0.051
|
||||
(0, False, True, 99),
|
||||
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
|
||||
])
|
||||
@@ -1290,6 +1290,137 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
||||
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_handle_stoploss_on_exchange_partial(
|
||||
mocker, default_conf_usdt, fee, is_short, limit_order) -> None:
|
||||
stop_order_dict = {'id': "101", "status": "open"}
|
||||
stoploss = MagicMock(return_value=stop_order_dict)
|
||||
enter_order = limit_order[entry_side(is_short)]
|
||||
exit_order = limit_order[exit_side(is_short)]
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value={
|
||||
'bid': 1.9,
|
||||
'ask': 2.2,
|
||||
'last': 1.9
|
||||
}),
|
||||
create_order=MagicMock(side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]),
|
||||
get_fee=fee,
|
||||
create_stoploss=stoploss
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = None
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.stoploss_order_id == "101"
|
||||
assert trade.amount == 30
|
||||
stop_order_dict.update({'id': "102"})
|
||||
# Stoploss on exchange is cancelled on exchange, but filled partially.
|
||||
# Must update trade amount to guarantee successful exit.
|
||||
stoploss_order_hit = MagicMock(return_value={
|
||||
'id': "101",
|
||||
'status': 'canceled',
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'filled': trade.amount / 2,
|
||||
'remaining': trade.amount / 2,
|
||||
'amount': enter_order['amount'],
|
||||
})
|
||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
# Stoploss filled partially ...
|
||||
assert trade.amount == 15
|
||||
|
||||
assert trade.stoploss_order_id == "102"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_handle_stoploss_on_exchange_partial_cancel_here(
|
||||
mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None:
|
||||
stop_order_dict = {'id': "101", "status": "open"}
|
||||
default_conf_usdt['trailing_stop'] = True
|
||||
stoploss = MagicMock(return_value=stop_order_dict)
|
||||
enter_order = limit_order[entry_side(is_short)]
|
||||
exit_order = limit_order[exit_side(is_short)]
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=MagicMock(return_value={
|
||||
'bid': 1.9,
|
||||
'ask': 2.2,
|
||||
'last': 1.9
|
||||
}),
|
||||
create_order=MagicMock(side_effect=[
|
||||
enter_order,
|
||||
exit_order,
|
||||
]),
|
||||
get_fee=fee,
|
||||
create_stoploss=stoploss
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = None
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert stoploss.call_count == 1
|
||||
assert trade.stoploss_order_id == "101"
|
||||
assert trade.amount == 30
|
||||
stop_order_dict.update({'id': "102"})
|
||||
# Stoploss on exchange is open.
|
||||
# Freqtrade cancels the stop - but cancel returns a partial filled order.
|
||||
stoploss_order_hit = MagicMock(return_value={
|
||||
'id': "101",
|
||||
'status': 'open',
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'filled': 0,
|
||||
'remaining': trade.amount,
|
||||
'amount': enter_order['amount'],
|
||||
})
|
||||
stoploss_order_cancel = MagicMock(return_value={
|
||||
'id': "101",
|
||||
'status': 'canceled',
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'filled': trade.amount / 2,
|
||||
'remaining': trade.amount / 2,
|
||||
'amount': enter_order['amount'],
|
||||
})
|
||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel)
|
||||
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-10).datetime
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
# Canceled Stoploss filled partially ...
|
||||
assert log_has_re('Cancelling current stoploss on exchange.*', caplog)
|
||||
|
||||
assert trade.stoploss_order_id == "102"
|
||||
assert trade.amount == 15
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
|
||||
limit_order) -> None:
|
||||
@@ -1671,7 +1802,7 @@ def test_stoploss_on_exchange_price_rounding(
|
||||
EXMS,
|
||||
get_fee=fee,
|
||||
)
|
||||
price_mock = MagicMock(side_effect=lambda p, s: int(s))
|
||||
price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s))
|
||||
stoploss_mock = MagicMock(return_value={'id': '13434334'})
|
||||
adjust_mock = MagicMock(return_value=False)
|
||||
mocker.patch.multiple(
|
||||
@@ -2824,6 +2955,9 @@ def test_manage_open_orders_exit_usercustom(
|
||||
assert rpc_mock.call_count == 2
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == 1
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
# cancelling didn't succeed - order-id remains open.
|
||||
assert trade.open_order_id is not None
|
||||
|
||||
# 2nd canceled trade - Fail execute exit
|
||||
caplog.clear()
|
||||
@@ -3334,6 +3468,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
||||
|
||||
# TODO: should not be magicmock
|
||||
trade = MagicMock()
|
||||
trade.open_order_id = '125'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
order = {'remaining': 1,
|
||||
'id': '125',
|
||||
@@ -3341,6 +3476,10 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
||||
'status': "open"}
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
|
||||
# mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order)
|
||||
# assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
# assert trade.open_order_id == '125'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short, open_rate, amt", [
|
||||
(False, 2.0, 30.0),
|
||||
|
Reference in New Issue
Block a user