Merge branch 'develop' into feat/short
This commit is contained in:
commit
463714832d
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -115,7 +115,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-latest ]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -207,7 +207,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest ]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -49,7 +49,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
|
||||
## Features
|
||||
|
||||
- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux.
|
||||
- [x] **Based on Python 3.8+**: For botting on any operating system - Windows, macOS and Linux.
|
||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||
- [x] **Dry-run**: Run the bot without paying money.
|
||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||
@ -197,7 +197,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||
|
||||
### Software requirements
|
||||
|
||||
- [Python >= 3.7](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||
|
Binary file not shown.
@ -5,9 +5,6 @@ python -m pip install --upgrade pip wheel
|
||||
|
||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
if ($pyv -eq '3.7') {
|
||||
pip install build_helpers\TA_Lib-0.4.24-cp37-cp37m-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.8') {
|
||||
pip install build_helpers\TA_Lib-0.4.24-cp38-cp38-win_amd64.whl
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ You can define your own estimator for Hyperopt by implementing `generate_estimat
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator():
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
return "RF"
|
||||
|
||||
```
|
||||
@ -119,13 +119,34 @@ Example for `ExtraTreesRegressor` ("ET") with additional parameters:
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator():
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
from skopt.learning import ExtraTreesRegressor
|
||||
# Corresponds to "ET" - but allows additional parameters.
|
||||
return ExtraTreesRegressor(n_estimators=100)
|
||||
|
||||
```
|
||||
|
||||
The `dimensions` parameter is the list of `skopt.space.Dimension` objects corresponding to the parameters to be optimized. It can be used to create isotropic kernels for the `skopt.learning.GaussianProcessRegressor` estimator. Here's an example:
|
||||
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
from skopt.utils import cook_estimator
|
||||
from skopt.learning.gaussian_process.kernels import (Matern, ConstantKernel)
|
||||
kernel_bounds = (0.0001, 10000)
|
||||
kernel = (
|
||||
ConstantKernel(1.0, kernel_bounds) *
|
||||
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=2.5)
|
||||
)
|
||||
kernel += (
|
||||
ConstantKernel(1.0, kernel_bounds) *
|
||||
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=1.5)
|
||||
)
|
||||
|
||||
return cook_estimator("GP", space=dimensions, kernel=kernel, n_restarts_optimizer=2)
|
||||
```
|
||||
|
||||
!!! Note
|
||||
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
|
||||
If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters.
|
||||
|
@ -175,6 +175,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
||||
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
||||
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
||||
|
||||
### Parameters in the strategy
|
||||
|
||||
@ -200,6 +201,7 @@ Values set in the configuration file always overwrite values set in the strategy
|
||||
* `ignore_roi_if_buy_signal`
|
||||
* `ignore_buying_expired_candle_after`
|
||||
* `position_adjustment_enable`
|
||||
* `max_entry_position_adjustment`
|
||||
|
||||
### Configuring amount per trade
|
||||
|
||||
|
@ -126,6 +126,12 @@ All freqtrade arguments will be available by running `docker-compose run --rm fr
|
||||
!!! Note "`docker-compose run --rm`"
|
||||
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
|
||||
|
||||
??? Note "Using docker without docker-compose"
|
||||
"`docker-compose run --rm`" will require a compose file to be provided.
|
||||
Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead.
|
||||
For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`.
|
||||
This can be useful for fetching exchange information to add to your `config.json` without affecting your running containers.
|
||||
|
||||
#### Example: Download data with docker-compose
|
||||
|
||||
Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host.
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
Freqtrade is a crypto-currency algorithmic trading software developed in python (3.7+) and supported on Windows, macOS and Linux.
|
||||
Freqtrade is a crypto-currency algorithmic trading software developed in python (3.8+) and supported on Windows, macOS and Linux.
|
||||
|
||||
!!! Danger "DISCLAIMER"
|
||||
This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||
@ -67,7 +67,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
||||
|
||||
Alternatively
|
||||
|
||||
- Python 3.7+
|
||||
- Python 3.8+
|
||||
- pip (pip3)
|
||||
- git
|
||||
- TA-Lib
|
||||
|
@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
||||
|
||||
### Install guide
|
||||
|
||||
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [Python >= 3.8.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||
|
@ -1,4 +1,4 @@
|
||||
mkdocs==1.2.3
|
||||
mkdocs-material==8.1.7
|
||||
mkdocs-material==8.1.8
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==9.1
|
||||
|
@ -363,8 +363,8 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: datetime,
|
||||
proposed_rate, **kwargs) -> float:
|
||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
|
||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||
timeframe=self.timeframe)
|
||||
@ -414,7 +414,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
|
||||
The function must return either `True` (cancel order) or `False` (keep order alive).
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
@ -427,22 +427,24 @@ class AwesomeStrategy(IStrategy):
|
||||
'sell': 60 * 25
|
||||
}
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
||||
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||
return True
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
|
||||
return True
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||
return True
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
|
||||
return True
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
@ -501,7 +503,7 @@ class AwesomeStrategy(IStrategy):
|
||||
# ... populate_* methods
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a entry order.
|
||||
@ -579,11 +581,13 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_
|
||||
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
||||
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging).
|
||||
|
||||
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
|
||||
|
||||
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased).
|
||||
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||
|
||||
This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
|
||||
This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`.
|
||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||
|
||||
!!! Note "About stake size"
|
||||
@ -614,14 +618,14 @@ class DigDeeperStrategy(IStrategy):
|
||||
# ... populate_* methods
|
||||
|
||||
# Example specific variables
|
||||
max_dca_orders = 3
|
||||
max_entry_position_adjustment = 3
|
||||
# This number is explained a bit further down
|
||||
max_dca_multiplier = 5.5
|
||||
|
||||
# This is called when placing the initial order (opening trade)
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
**kwargs) -> float:
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
|
||||
# We need to leave most of the funds for possible further DCA orders
|
||||
# This also applies to fixed stakes
|
||||
@ -656,8 +660,7 @@ class DigDeeperStrategy(IStrategy):
|
||||
return None
|
||||
|
||||
filled_buys = trade.select_filled_orders('buy')
|
||||
count_of_buys = len(filled_buys)
|
||||
|
||||
count_of_buys = trade.nr_of_successful_buys
|
||||
# Allow up to 3 additional increasingly larger buys (4 in total)
|
||||
# Initial buy is 1x
|
||||
# If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
|
||||
@ -666,15 +669,14 @@ class DigDeeperStrategy(IStrategy):
|
||||
# Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake.
|
||||
# That is why max_dca_multiplier is 5.5
|
||||
# Hope you have a deep wallet!
|
||||
if 0 < count_of_buys <= self.max_dca_orders:
|
||||
try:
|
||||
# This returns first order stake size
|
||||
stake_amount = filled_buys[0].cost
|
||||
# This then calculates current safety order size
|
||||
stake_amount = stake_amount * (1 + (count_of_buys * 0.25))
|
||||
return stake_amount
|
||||
except Exception as exception:
|
||||
return None
|
||||
try:
|
||||
# This returns first order stake size
|
||||
stake_amount = filled_buys[0].cost
|
||||
# This then calculates current safety order size
|
||||
stake_amount = stake_amount * (1 + (count_of_buys * 0.25))
|
||||
return stake_amount
|
||||
except Exception as exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
@ -25,7 +25,7 @@ Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7
|
||||
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.24-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
||||
|
||||
Freqtrade provides these dependencies for the latest 3 Python versions (3.7, 3.8, 3.9 and 3.10) and for 64bit Windows.
|
||||
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows.
|
||||
Other versions must be downloaded from the above link.
|
||||
|
||||
``` powershell
|
||||
|
@ -4,7 +4,7 @@ channels:
|
||||
# - defaults
|
||||
dependencies:
|
||||
# 1/4 req main
|
||||
- python>=3.7,<3.9
|
||||
- python>=3.8,<=3.10
|
||||
- numpy
|
||||
- pandas
|
||||
- pip
|
||||
@ -25,10 +25,14 @@ dependencies:
|
||||
- fastapi
|
||||
- uvicorn
|
||||
- pyjwt
|
||||
- aiofiles
|
||||
- psutil
|
||||
- colorama
|
||||
- questionary
|
||||
- prompt-toolkit
|
||||
- schedule
|
||||
- python-dateutil
|
||||
|
||||
|
||||
# ============================
|
||||
# 2/4 req dev
|
||||
|
@ -3,7 +3,7 @@
|
||||
__main__.py for Freqtrade
|
||||
To launch Freqtrade as a module
|
||||
|
||||
> python -m freqtrade (with Python >= 3.7)
|
||||
> python -m freqtrade (with Python >= 3.8)
|
||||
"""
|
||||
|
||||
from freqtrade import main
|
||||
|
@ -377,7 +377,9 @@ CONF_SCHEMA = {
|
||||
'type': 'string',
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'jsongz'
|
||||
}
|
||||
},
|
||||
'position_adjustment_enable': {'type': 'boolean'},
|
||||
'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1},
|
||||
},
|
||||
'definitions': {
|
||||
'exchange': {
|
||||
|
@ -1114,7 +1114,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_tickers(self, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
"""
|
||||
:param cached: Allow cached result
|
||||
:return: fetch_tickers result
|
||||
@ -1124,7 +1124,7 @@ class Exchange:
|
||||
if tickers:
|
||||
return tickers
|
||||
try:
|
||||
tickers = self._api.fetch_tickers()
|
||||
tickers = self._api.fetch_tickers(symbols)
|
||||
self._fetch_tickers_cache['fetch_tickers'] = tickers
|
||||
return tickers
|
||||
except ccxt.NotSupported as e:
|
||||
|
@ -43,6 +43,12 @@ class Kraken(Exchange):
|
||||
return (parent_check and
|
||||
market.get('darkpool', False) is False)
|
||||
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
# Only fetch tickers for current stake currency
|
||||
# Otherwise the request for kraken becomes too large.
|
||||
symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']]))
|
||||
return super().get_tickers(symbols=symbols, cached=cached)
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> dict:
|
||||
if self._config['dry_run']:
|
||||
|
@ -9,7 +9,6 @@ from math import isclose
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from schedule import Scheduler
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
@ -521,8 +520,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
try:
|
||||
self.check_and_call_adjust_trade_position(trade)
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to adjust position of trade for %s: %s',
|
||||
trade.pair, exception)
|
||||
logger.warning(
|
||||
f"Unable to adjust position of trade for {trade.pair}: {exception}")
|
||||
|
||||
def check_and_call_adjust_trade_position(self, trade: Trade):
|
||||
"""
|
||||
@ -531,6 +530,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
Once that completes, the existing trade is modified to match new data.
|
||||
"""
|
||||
# TODO-lev: Check what changes are necessary for DCA in relation to shorts.
|
||||
if self.strategy.max_entry_position_adjustment > -1:
|
||||
count_of_buys = trade.nr_of_successful_buys
|
||||
if count_of_buys > self.strategy.max_entry_position_adjustment:
|
||||
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
|
||||
return
|
||||
else:
|
||||
logger.debug("Max adjustment entries is set to unlimited.")
|
||||
current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy")
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
@ -649,7 +655,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
pos_adjust = trade is not None
|
||||
|
||||
enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake(
|
||||
pair, price, stake_amount, side, trade_side, trade)
|
||||
pair, price, stake_amount, side, trade_side, enter_tag, trade)
|
||||
|
||||
if not stake_amount:
|
||||
return False
|
||||
@ -680,8 +686,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
|
||||
side=trade_side
|
||||
):
|
||||
entry_tag=enter_tag, side=trade_side):
|
||||
logger.info(f"User requested abortion of buying {pair}")
|
||||
return False
|
||||
amount = self.exchange.amount_to_precision(pair, amount)
|
||||
@ -814,6 +819,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
def get_valid_enter_price_and_stake(
|
||||
self, pair: str, price: Optional[float], stake_amount: float,
|
||||
side: str, trade_side: str,
|
||||
entry_tag: Optional[str],
|
||||
trade: Optional[Trade]) -> Tuple[float, float]:
|
||||
if price:
|
||||
enter_limit_requested = price
|
||||
@ -823,7 +829,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=proposed_enter_rate)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
proposed_rate=proposed_enter_rate)
|
||||
proposed_rate=proposed_enter_rate, entry_tag=entry_tag)
|
||||
|
||||
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||
|
||||
@ -844,7 +850,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||
min_stake=min_stake_amount, max_stake=max_stake_amount,
|
||||
side=trade_side
|
||||
entry_tag=entry_tag, side=trade_side
|
||||
)
|
||||
|
||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
@ -1145,20 +1151,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_timed_out(self, side: str, order: dict) -> bool:
|
||||
"""
|
||||
Check if timeout is active, and if the order is still open and timed out
|
||||
"""
|
||||
timeout = self.config.get('unfilledtimeout', {}).get(side)
|
||||
ordertime = arrow.get(order['datetime']).datetime
|
||||
if timeout is not None:
|
||||
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
|
||||
timeout_kwargs = {timeout_unit: -timeout}
|
||||
timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime
|
||||
return (order['status'] == 'open' and order['side'] == side
|
||||
and ordertime < timeout_threshold)
|
||||
return False
|
||||
|
||||
def check_handle_timedout(self) -> None:
|
||||
"""
|
||||
Check if any orders are timed out and cancel if necessary
|
||||
@ -1178,18 +1170,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
||||
is_entering = order['side'] == trade.enter_side
|
||||
not_closed = order['status'] == 'open' or fully_cancelled
|
||||
side = trade.enter_side if is_entering else trade.exit_side
|
||||
timed_out = self._check_timed_out(side, order)
|
||||
time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout'
|
||||
time_method = 'sell' if order['side'] == 'sell' else 'buy'
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
|
||||
if not_closed and (fully_cancelled or timed_out or (
|
||||
strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
|
||||
pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order
|
||||
)
|
||||
)):
|
||||
if not_closed and (fully_cancelled or self.strategy.ft_check_timed_out(
|
||||
time_method, trade, order, datetime.now(timezone.utc))
|
||||
):
|
||||
if is_entering:
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
else:
|
||||
|
@ -9,8 +9,8 @@ from typing import Any, List
|
||||
|
||||
|
||||
# check min. python version
|
||||
if sys.version_info < (3, 7): # pragma: no cover
|
||||
sys.exit("Freqtrade requires Python version >= 3.7")
|
||||
if sys.version_info < (3, 8): # pragma: no cover
|
||||
sys.exit("Freqtrade requires Python version >= 3.8")
|
||||
|
||||
from freqtrade.commands import Arguments
|
||||
from freqtrade.exceptions import FreqtradeException, OperationalException
|
||||
|
@ -434,7 +434,12 @@ class Backtesting:
|
||||
|
||||
# Check if we need to adjust our current positions
|
||||
if self.strategy.position_adjustment_enable:
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row)
|
||||
check_adjust_buy = True
|
||||
if self.strategy.max_entry_position_adjustment > -1:
|
||||
count_of_buys = trade.nr_of_successful_buys
|
||||
check_adjust_buy = (count_of_buys <= self.strategy.max_entry_position_adjustment)
|
||||
if check_adjust_buy:
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row)
|
||||
|
||||
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
|
||||
enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX]
|
||||
@ -533,12 +538,14 @@ class Backtesting:
|
||||
def _enter_trade(self, pair: str, row: Tuple, direction: str,
|
||||
stake_amount: Optional[float] = None,
|
||||
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
|
||||
|
||||
current_time = row[DATE_IDX].to_pydatetime()
|
||||
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
|
||||
# let's call the custom entry price, using the open price as default price
|
||||
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=row[OPEN_IDX])(
|
||||
pair=pair, current_time=current_time,
|
||||
proposed_rate=row[OPEN_IDX]) # default value is the open rate
|
||||
proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate
|
||||
|
||||
# Move rate to within the candle's low/high rate
|
||||
propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX])
|
||||
@ -557,7 +564,7 @@ class Backtesting:
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=current_time, current_rate=propose_rate,
|
||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
|
||||
side=direction)
|
||||
entry_tag=entry_tag, side=direction)
|
||||
|
||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
@ -585,14 +592,13 @@ class Backtesting:
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
|
||||
time_in_force=time_in_force, current_time=current_time,
|
||||
side=direction):
|
||||
entry_tag=entry_tag, side=direction):
|
||||
return None
|
||||
|
||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||
amount = round((stake_amount / propose_rate) * leverage, 8)
|
||||
if trade is None:
|
||||
# Enter trade
|
||||
has_buy_tag = len(row) >= ENTER_TAG_IDX + 1
|
||||
trade = LocalTrade(
|
||||
pair=pair,
|
||||
open_rate=propose_rate,
|
||||
@ -602,7 +608,7 @@ class Backtesting:
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
enter_tag=row[ENTER_TAG_IDX] if has_buy_tag else None,
|
||||
enter_tag=entry_tag,
|
||||
exchange=self._exchange_name,
|
||||
is_short=(direction == 'short'),
|
||||
trading_mode=self.trading_mode,
|
||||
@ -619,6 +625,9 @@ class Backtesting:
|
||||
side="buy",
|
||||
order_type="market",
|
||||
status="closed",
|
||||
order_date=current_time,
|
||||
order_filled_date=current_time,
|
||||
order_update_date=current_time,
|
||||
price=propose_rate,
|
||||
average=propose_rate,
|
||||
amount=amount,
|
||||
|
@ -367,7 +367,7 @@ class Hyperopt:
|
||||
}
|
||||
|
||||
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
|
||||
estimator = self.custom_hyperopt.generate_estimator()
|
||||
estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions)
|
||||
|
||||
acq_optimizer = "sampling"
|
||||
if isinstance(estimator, str):
|
||||
|
@ -91,5 +91,5 @@ class HyperOptAuto(IHyperOpt):
|
||||
def trailing_space(self) -> List['Dimension']:
|
||||
return self._get_func('trailing_space')()
|
||||
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
return self._get_func('generate_estimator')()
|
||||
def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
|
||||
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)
|
||||
|
@ -40,7 +40,7 @@ class IHyperOpt(ABC):
|
||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||
IHyperOpt.timeframe = str(config['timeframe'])
|
||||
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
|
||||
"""
|
||||
Return base_estimator.
|
||||
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
|
||||
|
@ -804,18 +804,19 @@ class LocalTrade():
|
||||
|
||||
total_amount = 0.0
|
||||
total_stake = 0.0
|
||||
for temp_order in self.orders:
|
||||
if (temp_order.ft_is_open or
|
||||
(temp_order.ft_order_side != self.enter_side) or
|
||||
(temp_order.status not in NON_OPEN_EXCHANGE_STATES)):
|
||||
for o in self.orders:
|
||||
if (o.ft_is_open or
|
||||
(o.ft_order_side != self.enter_side) or
|
||||
(o.status not in NON_OPEN_EXCHANGE_STATES)):
|
||||
continue
|
||||
|
||||
tmp_amount = temp_order.amount
|
||||
if temp_order.filled is not None:
|
||||
tmp_amount = temp_order.filled
|
||||
if tmp_amount > 0.0 and temp_order.average is not None:
|
||||
tmp_amount = o.amount
|
||||
tmp_price = o.average or o.price
|
||||
if o.filled is not None:
|
||||
tmp_amount = o.filled
|
||||
if tmp_amount > 0.0 and tmp_price is not None:
|
||||
total_amount += tmp_amount
|
||||
total_stake += temp_order.average * tmp_amount
|
||||
total_stake += tmp_price * tmp_amount
|
||||
|
||||
if total_amount > 0:
|
||||
self.open_rate = total_stake / total_amount
|
||||
|
@ -97,7 +97,8 @@ class StrategyResolver(IResolver):
|
||||
("sell_profit_offset", 0.0),
|
||||
("disable_dataframe_checks", False),
|
||||
("ignore_buying_expired_candle_after", 0),
|
||||
("position_adjustment_enable", False)
|
||||
("position_adjustment_enable", False),
|
||||
("max_entry_position_adjustment", -1),
|
||||
]
|
||||
for attribute, default in attributes:
|
||||
StrategyResolver._override_attribute_helper(strategy, config,
|
||||
|
@ -175,6 +175,8 @@ class ShowConfig(BaseModel):
|
||||
bot_name: str
|
||||
state: str
|
||||
runmode: str
|
||||
position_adjustment_enable: bool
|
||||
max_entry_position_adjustment: int
|
||||
|
||||
|
||||
class TradeSchema(BaseModel):
|
||||
|
@ -77,6 +77,9 @@ class CryptoToFiatConverter:
|
||||
else:
|
||||
return None
|
||||
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
|
||||
if crypto_symbol == 'eth':
|
||||
found = [x for x in self._coinlistings if x['id'] == 'ethereum']
|
||||
|
||||
if len(found) == 1:
|
||||
return found[0]['id']
|
||||
|
||||
|
@ -138,7 +138,12 @@ class RPC:
|
||||
'ask_strategy': config.get('ask_strategy', {}),
|
||||
'bid_strategy': config.get('bid_strategy', {}),
|
||||
'state': str(botstate),
|
||||
'runmode': config['runmode'].value
|
||||
'runmode': config['runmode'].value,
|
||||
'position_adjustment_enable': config.get('position_adjustment_enable', False),
|
||||
'max_entry_position_adjustment': (
|
||||
config.get('max_entry_position_adjustment', -1)
|
||||
if config.get('max_entry_position_adjustment') != float('inf')
|
||||
else -1)
|
||||
}
|
||||
return val
|
||||
|
||||
@ -252,8 +257,9 @@ class RPC:
|
||||
profit_str
|
||||
]
|
||||
if self._config.get('position_adjustment_enable', False):
|
||||
filled_buys = trade.select_filled_orders('buy')
|
||||
detail_trade.append(str(len(filled_buys)))
|
||||
max_buy = self._config['max_entry_position_adjustment'] + 1
|
||||
filled_buys = trade.nr_of_successful_buys
|
||||
detail_trade.append(f"{filled_buys}/{max_buy}")
|
||||
trades_list.append(detail_trade)
|
||||
profitcol = "Profit"
|
||||
if self._fiat_converter:
|
||||
|
@ -1363,6 +1363,14 @@ class Telegram(RPCHandler):
|
||||
else:
|
||||
sl_info = f"*Stoploss:* `{val['stoploss']}`\n"
|
||||
|
||||
if val['position_adjustment_enable']:
|
||||
pa_info = (
|
||||
f"*Position adjustment:* On\n"
|
||||
f"*Max enter position adjustment:* `{val['max_entry_position_adjustment']}`\n"
|
||||
)
|
||||
else:
|
||||
pa_info = "*Position adjustment:* Off\n"
|
||||
|
||||
self._send_msg(
|
||||
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
|
||||
f"*Exchange:* `{val['exchange']}`\n"
|
||||
@ -1372,6 +1380,7 @@ class Telegram(RPCHandler):
|
||||
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
|
||||
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
|
||||
f"{sl_info}"
|
||||
f"{pa_info}"
|
||||
f"*Timeframe:* `{val['timeframe']}`\n"
|
||||
f"*Strategy:* `{val['strategy']}`\n"
|
||||
f"*Current state:* `{val['state']}`"
|
||||
|
@ -108,6 +108,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
# Position adjustment is disabled by default
|
||||
position_adjustment_enable: bool = False
|
||||
max_entry_position_adjustment: int = -1
|
||||
|
||||
# Number of seconds after which the candle will no longer result in a buy on expired candles
|
||||
ignore_buying_expired_candle_after: int = 0
|
||||
@ -188,7 +189,17 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
return dataframe
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||
def bot_loop_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Called at the start of the bot iteration (one loop).
|
||||
Might be used to perform pair-independent tasks
|
||||
(e.g. gather some remote resource for comparison)
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Check buy timeout function callback.
|
||||
This method can be used to override the enter-timeout.
|
||||
@ -201,12 +212,14 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param pair: Pair the trade is for
|
||||
:param trade: trade object.
|
||||
:param order: Order dictionary as returned from CCXT.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the entry order is cancelled.
|
||||
"""
|
||||
return False
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Check sell timeout function callback.
|
||||
This method can be used to override the exit-timeout.
|
||||
@ -219,22 +232,14 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param pair: Pair the trade is for
|
||||
:param trade: trade object.
|
||||
:param order: Order dictionary as returned from CCXT.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled.
|
||||
"""
|
||||
return False
|
||||
|
||||
def bot_loop_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Called at the start of the bot iteration (one loop).
|
||||
Might be used to perform pair-independent tasks
|
||||
(e.g. gather some remote resource for comparison)
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
pass
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a entry order.
|
||||
@ -251,6 +256,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||
@ -309,7 +315,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
return self.stoploss
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||
**kwargs) -> float:
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
"""
|
||||
Custom entry price logic, returning the new entry price.
|
||||
|
||||
@ -320,6 +326,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
"""
|
||||
@ -371,7 +378,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
side: str, **kwargs) -> float:
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
|
||||
@ -381,6 +388,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param proposed_stake: A stake amount proposed by the bot.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:return: A stake size, which is between min_stake and max_stake.
|
||||
"""
|
||||
@ -392,6 +400,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
Only called when `position_adjustment_enable` is set to True.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
@ -976,6 +985,29 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def ft_check_timed_out(self, side: str, trade: Trade, order: Dict,
|
||||
current_time: datetime) -> bool:
|
||||
"""
|
||||
FT Internal method.
|
||||
Check if timeout is active, and if the order is still open and timed out
|
||||
"""
|
||||
timeout = self.config.get('unfilledtimeout', {}).get(side)
|
||||
ordertime = arrow.get(order['datetime']).datetime
|
||||
if timeout is not None:
|
||||
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
|
||||
timeout_kwargs = {timeout_unit: -timeout}
|
||||
timeout_threshold = current_time + timedelta(**timeout_kwargs)
|
||||
timedout = (order['status'] == 'open' and order['side'] == side
|
||||
and ordertime < timeout_threshold)
|
||||
if timedout:
|
||||
return True
|
||||
time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout
|
||||
|
||||
return strategy_safe_wrapper(time_method,
|
||||
default_retval=False)(
|
||||
pair=trade.pair, trade=trade, order=order,
|
||||
current_time=current_time)
|
||||
|
||||
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Populates indicators for given candle (OHLCV) data (for multiple pairs)
|
||||
|
@ -12,9 +12,47 @@ def bot_loop_start(self, **kwargs) -> None:
|
||||
"""
|
||||
pass
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float,
|
||||
entry_tag: 'Optional[str]', **kwargs) -> float:
|
||||
"""
|
||||
Custom entry price logic, returning the new entry price.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns None, orderbook is used to set entry price
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
"""
|
||||
return proposed_rate
|
||||
|
||||
def custom_exit_price(self, pair: str, trade: 'Trade',
|
||||
current_time: 'datetime', proposed_rate: float,
|
||||
current_profit: float, **kwargs) -> float:
|
||||
"""
|
||||
Custom exit price logic, returning the new exit price.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns None, orderbook is used to set exit price
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New exit price value if provided
|
||||
"""
|
||||
return proposed_rate
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
side: str, **kwargs) -> float:
|
||||
side: str, entry_tag: 'Optional[str]', **kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
|
||||
@ -24,6 +62,7 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f
|
||||
:param proposed_stake: A stake amount proposed by the bot.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:return: A stake size, which is between min_stake and max_stake.
|
||||
"""
|
||||
@ -78,7 +117,7 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
|
||||
return None
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime,
|
||||
time_in_force: str, current_time: datetime, entry_tag: 'Optional[str]',
|
||||
side: str, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a entry order.
|
||||
@ -95,6 +134,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||
@ -169,3 +209,26 @@ def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
|
||||
:return bool: When True is returned, then the sell-order is cancelled.
|
||||
"""
|
||||
return False
|
||||
|
||||
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
||||
current_rate: float, current_profit: float, min_stake: float,
|
||||
max_stake: float, **kwargs) -> 'Optional[float]':
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
Only called when `position_adjustment_enable` is set to True.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns None
|
||||
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Current buy rate.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: Stake amount to adjust your trade
|
||||
"""
|
||||
return None
|
||||
|
@ -8,7 +8,7 @@ flake8==4.0.1
|
||||
flake8-tidy-imports==4.6.0
|
||||
mypy==0.931
|
||||
pytest==6.2.5
|
||||
pytest-asyncio==0.17.1
|
||||
pytest-asyncio==0.17.2
|
||||
pytest-cov==3.0.0
|
||||
pytest-mock==3.6.1
|
||||
pytest-random-order==1.0.4
|
||||
@ -21,9 +21,9 @@ nbconvert==6.4.0
|
||||
|
||||
# mypy types
|
||||
types-cachetools==4.2.9
|
||||
types-filelock==3.2.4
|
||||
types-filelock==3.2.5
|
||||
types-requests==2.27.7
|
||||
types-tabulate==0.8.5
|
||||
|
||||
# Extensions to datetime library
|
||||
types-python-dateutil==2.8.7
|
||||
types-python-dateutil==2.8.8
|
@ -1,13 +1,12 @@
|
||||
numpy==1.21.5; python_version <= '3.7'
|
||||
numpy==1.22.1; python_version > '3.7'
|
||||
pandas==1.3.5
|
||||
numpy==1.22.1
|
||||
pandas==1.4.0
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==1.68.20
|
||||
ccxt==1.70.45
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
cryptography==36.0.1
|
||||
aiohttp==3.8.1
|
||||
SQLAlchemy==1.4.29
|
||||
SQLAlchemy==1.4.31
|
||||
python-telegram-bot==13.10
|
||||
arrow==1.2.1
|
||||
cachetools==4.2.2
|
||||
@ -32,7 +31,7 @@ python-rapidjson==1.5
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.72.0
|
||||
fastapi==0.73.0
|
||||
uvicorn==0.17.0
|
||||
pyjwt==2.3.0
|
||||
aiofiles==0.8.0
|
||||
|
@ -14,7 +14,6 @@ classifiers =
|
||||
Environment :: Console
|
||||
Intended Audience :: Science/Research
|
||||
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
|
6
setup.sh
6
setup.sh
@ -25,7 +25,7 @@ function check_installed_python() {
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for v in 9 10 8 7
|
||||
for v in 9 10 8
|
||||
do
|
||||
PYTHON="python3.${v}"
|
||||
which $PYTHON
|
||||
@ -219,7 +219,7 @@ function install() {
|
||||
install_redhat
|
||||
else
|
||||
echo "This script does not support your OS."
|
||||
echo "If you have Python version 3.7 - 3.10, pip, virtualenv, ta-lib you can continue."
|
||||
echo "If you have Python version 3.8 - 3.10, pip, virtualenv, ta-lib you can continue."
|
||||
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||
sleep 10
|
||||
fi
|
||||
@ -246,7 +246,7 @@ function help() {
|
||||
echo " -p,--plot Install dependencies for Plotting scripts."
|
||||
}
|
||||
|
||||
# Verify if 3.7 or 3.8 is installed
|
||||
# Verify if 3.8+ is installed
|
||||
check_installed_python
|
||||
|
||||
case $* in
|
||||
|
@ -782,6 +782,8 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
||||
# While buy-signals are unrealistic, running backtesting
|
||||
# over and over again should not cause different results
|
||||
for [contour, numres] in tests:
|
||||
# Debug output for random test failure
|
||||
print(f"{contour}, {numres}")
|
||||
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres
|
||||
|
||||
|
||||
|
@ -148,10 +148,13 @@ def test_fiat_multiple_coins(mocker, caplog):
|
||||
{'id': 'helium', 'symbol': 'hnt', 'name': 'Helium'},
|
||||
{'id': 'hymnode', 'symbol': 'hnt', 'name': 'Hymnode'},
|
||||
{'id': 'bitcoin', 'symbol': 'btc', 'name': 'Bitcoin'},
|
||||
{'id': 'ethereum', 'symbol': 'eth', 'name': 'Ethereum'},
|
||||
{'id': 'ethereum-wormhole', 'symbol': 'eth', 'name': 'Ethereum Wormhole'},
|
||||
]
|
||||
|
||||
assert fiat_convert._get_gekko_id('btc') == 'bitcoin'
|
||||
assert fiat_convert._get_gekko_id('hnt') is None
|
||||
assert fiat_convert._get_gekko_id('eth') == 'ethereum'
|
||||
|
||||
assert log_has('Found multiple mappings in goingekko for hnt.', caplog)
|
||||
|
||||
|
@ -44,7 +44,7 @@ def test_strategy_test_v3(result, fee, is_short, side):
|
||||
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
|
||||
rate=20000, time_in_force='gtc',
|
||||
current_time=datetime.utcnow(),
|
||||
side=side) is True
|
||||
side=side, entry_tag=None) is True
|
||||
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
|
||||
rate=20000, time_in_force='gtc', sell_reason='roi',
|
||||
current_time=datetime.utcnow(),
|
||||
|
@ -5182,3 +5182,32 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
# Make sure the closed order is found as the second order.
|
||||
order = trade.select_order('buy', False)
|
||||
assert order.order_id == '652'
|
||||
|
||||
|
||||
def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||
default_conf_usdt.update({
|
||||
"position_adjustment_enable": True,
|
||||
})
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.check_and_call_adjust_trade_position',
|
||||
side_effect=DependencyException())
|
||||
|
||||
create_mock_trades(fee)
|
||||
|
||||
freqtrade.process_open_trade_positions()
|
||||
assert log_has_re(r"Unable to adjust position of trade for .*", caplog)
|
||||
|
||||
|
||||
def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, caplog) -> None:
|
||||
default_conf_usdt.update({
|
||||
"position_adjustment_enable": True,
|
||||
"max_entry_position_adjustment": 0,
|
||||
})
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
create_mock_trades(fee)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
freqtrade.process_open_trade_positions()
|
||||
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)
|
||||
|
@ -243,6 +243,8 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 2
|
||||
for o in trade.orders:
|
||||
assert o.status == "closed"
|
||||
assert trade.stake_amount == 120
|
||||
|
||||
# Open-rate averaged between 2.0 and 2.0 * 0.995
|
||||
@ -258,7 +260,6 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid']
|
||||
|
||||
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
|
||||
|
||||
assert trade.nr_of_successful_buys == 2
|
||||
|
||||
# Sell
|
||||
|
@ -2464,6 +2464,33 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
|
||||
assert trade.fee_open_cost == 2 * o1_fee_cost
|
||||
assert trade.open_trade_value == 2 * o1_trade_val
|
||||
assert trade.nr_of_successful_buys == 2
|
||||
# Check with 1 order
|
||||
order_noavg = Order(
|
||||
ft_order_side='buy',
|
||||
ft_pair=trade.pair,
|
||||
ft_is_open=False,
|
||||
status="closed",
|
||||
symbol=trade.pair,
|
||||
order_type="market",
|
||||
side="buy",
|
||||
price=o1_rate,
|
||||
average=None,
|
||||
filled=o1_amount,
|
||||
remaining=0,
|
||||
cost=o1_amount,
|
||||
order_date=trade.open_date,
|
||||
order_filled_date=trade.open_date,
|
||||
)
|
||||
trade.orders.append(order_noavg)
|
||||
trade.recalc_trade_from_orders()
|
||||
|
||||
# Calling recalc with single initial order should not change anything
|
||||
assert trade.amount == 3 * o1_amount
|
||||
assert trade.stake_amount == 3 * o1_amount
|
||||
assert trade.open_rate == o1_rate
|
||||
assert trade.fee_open_cost == 3 * o1_fee_cost
|
||||
assert trade.open_trade_value == 3 * o1_trade_val
|
||||
assert trade.nr_of_successful_buys == 3
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
|
Loading…
Reference in New Issue
Block a user