Merge branch 'develop' into feat/short
This commit is contained in:
commit
edd80c3006
@ -57,7 +57,11 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||||
|
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||||
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
||||||
|
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||||
|
|
||||||
* Generate backtest report output
|
* Generate backtest report output
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -36,6 +36,10 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
|||||||
|
|
||||||
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
|
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
|
||||||
|
|
||||||
|
!!! Note "ARM64 systems"
|
||||||
|
If you are running an ARM64 system (like a MacOS M1 or an Oracle VM), please use [docker](docker_quickstart.md) to run freqtrade.
|
||||||
|
While native installation is possible with some manual effort, this is not supported at the moment.
|
||||||
|
|
||||||
### Install guide
|
### Install guide
|
||||||
|
|
||||||
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.3
|
mkdocs==1.2.3
|
||||||
mkdocs-material==8.0.1
|
mkdocs-material==8.0.4
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.1
|
pymdown-extensions==9.1
|
||||||
|
@ -127,6 +127,21 @@ The provided exit-tag is then used as sell-reason - and shown as such in backtes
|
|||||||
!!! Note
|
!!! Note
|
||||||
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
|
## Strategy version
|
||||||
|
|
||||||
|
You can implement custom strategy versioning by using the "version" method, and returning the version you would like this strategy to have.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def version(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns version of the strategy.
|
||||||
|
"""
|
||||||
|
return "1.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
You should make sure to implement proper version control (like a git repository) alongside this, as freqtrade will not keep historic versions of your strategy, so it's up to the user to be able to eventually roll back to a prior version of the strategy.
|
||||||
|
|
||||||
## Derived strategies
|
## Derived strategies
|
||||||
|
|
||||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||||
|
@ -388,8 +388,10 @@ class AwesomeStrategy(IStrategy):
|
|||||||
**Example**:
|
**Example**:
|
||||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
||||||
|
|
||||||
!!! Warning "No backtesting support"
|
!!! Warning "Backtesting"
|
||||||
Custom entry-prices are currently not supported during backtesting.
|
While Custom prices are supported in backtesting (starting with 2021.12), prices will be moved to within the candle's high/low prices.
|
||||||
|
This behavior is currently being tested, and might be changed at a later point.
|
||||||
|
`custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices.
|
||||||
|
|
||||||
## Custom order timeout rules
|
## Custom order timeout rules
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from freqtrade.enums.backteststate import BacktestState
|
from freqtrade.enums.backteststate import BacktestState
|
||||||
from freqtrade.enums.candletype import CandleType
|
from freqtrade.enums.candletype import CandleType
|
||||||
from freqtrade.enums.collateral import Collateral
|
from freqtrade.enums.collateral import Collateral
|
||||||
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.selltype import SellType
|
from freqtrade.enums.selltype import SellType
|
||||||
|
6
freqtrade/enums/ordertypevalue.py
Normal file
6
freqtrade/enums/ordertypevalue.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTypeValues(str, Enum):
|
||||||
|
limit = 'limit'
|
||||||
|
market = 'market'
|
@ -356,10 +356,7 @@ class Backtesting:
|
|||||||
# use Open rate if open_rate > calculated sell rate
|
# use Open rate if open_rate > calculated sell rate
|
||||||
return sell_row[OPEN_IDX]
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
# Use the maximum between close_rate and low as we
|
return close_rate
|
||||||
# cannot sell outside of a candle.
|
|
||||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
|
||||||
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This should not be reached...
|
# This should not be reached...
|
||||||
@ -387,6 +384,17 @@ class Backtesting:
|
|||||||
|
|
||||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
|
# call the custom exit price,with default value as previous closerate
|
||||||
|
current_profit = trade.calc_profit_ratio(closerate)
|
||||||
|
if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL):
|
||||||
|
# Custom exit pricing only for sell-signals
|
||||||
|
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
|
default_retval=closerate)(
|
||||||
|
pair=trade.pair, trade=trade,
|
||||||
|
current_time=sell_row[DATE_IDX],
|
||||||
|
proposed_rate=closerate, current_profit=current_profit)
|
||||||
|
# Use the maximum between close_rate and low as we cannot sell outside of a candle.
|
||||||
|
closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
||||||
|
|
||||||
# Confirm trade exit:
|
# Confirm trade exit:
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
@ -449,12 +457,22 @@ class Backtesting:
|
|||||||
except DependencyException:
|
except DependencyException:
|
||||||
return None
|
return None
|
||||||
current_time = row[DATE_IDX].to_pydatetime()
|
current_time = row[DATE_IDX].to_pydatetime()
|
||||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) or 0
|
|
||||||
|
# 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=row[DATE_IDX].to_pydatetime(),
|
||||||
|
proposed_rate=row[OPEN_IDX]) # 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])
|
||||||
|
|
||||||
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
|
||||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||||
|
|
||||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||||
default_retval=stake_amount)(
|
default_retval=stake_amount)(
|
||||||
pair=pair, current_time=current_time, current_rate=row[OPEN_IDX],
|
pair=pair, current_time=current_time, current_rate=propose_rate,
|
||||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
|
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
|
||||||
side=direction)
|
side=direction)
|
||||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||||
@ -478,7 +496,7 @@ class Backtesting:
|
|||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
# Confirm trade entry:
|
# Confirm trade entry:
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX],
|
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
|
||||||
time_in_force=time_in_force, current_time=current_time,
|
time_in_force=time_in_force, current_time=current_time,
|
||||||
side=direction):
|
side=direction):
|
||||||
return None
|
return None
|
||||||
@ -491,7 +509,7 @@ class Backtesting:
|
|||||||
open_rate=row[OPEN_IDX],
|
open_rate=row[OPEN_IDX],
|
||||||
open_date=current_time,
|
open_date=current_time,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=round((stake_amount / row[OPEN_IDX]) * leverage, 8),
|
amount=round((stake_amount / propose_rate) * leverage, 8),
|
||||||
fee_open=self.fee,
|
fee_open=self.fee,
|
||||||
fee_close=self.fee,
|
fee_close=self.fee,
|
||||||
is_open=True,
|
is_open=True,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
|
from freqtrade.enums import OrderTypeValues
|
||||||
|
|
||||||
|
|
||||||
class Ping(BaseModel):
|
class Ping(BaseModel):
|
||||||
@ -132,11 +132,6 @@ class UnfilledTimeout(BaseModel):
|
|||||||
exit_timeout_count: Optional[int]
|
exit_timeout_count: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class OrderTypeValues(str, Enum):
|
|
||||||
limit = 'limit'
|
|
||||||
market = 'market'
|
|
||||||
|
|
||||||
|
|
||||||
class OrderTypes(BaseModel):
|
class OrderTypes(BaseModel):
|
||||||
buy: OrderTypeValues
|
buy: OrderTypeValues
|
||||||
sell: OrderTypeValues
|
sell: OrderTypeValues
|
||||||
@ -150,6 +145,7 @@ class OrderTypes(BaseModel):
|
|||||||
|
|
||||||
class ShowConfig(BaseModel):
|
class ShowConfig(BaseModel):
|
||||||
version: str
|
version: str
|
||||||
|
strategy_version: Optional[str]
|
||||||
api_version: float
|
api_version: float
|
||||||
dry_run: bool
|
dry_run: bool
|
||||||
trading_mode: str
|
trading_mode: str
|
||||||
|
@ -122,9 +122,11 @@ def edge(rpc: RPC = Depends(get_rpc)):
|
|||||||
@router.get('/show_config', response_model=ShowConfig, tags=['info'])
|
@router.get('/show_config', response_model=ShowConfig, tags=['info'])
|
||||||
def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)):
|
def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)):
|
||||||
state = ''
|
state = ''
|
||||||
|
strategy_version = None
|
||||||
if rpc:
|
if rpc:
|
||||||
state = rpc._freqtrade.state
|
state = rpc._freqtrade.state
|
||||||
resp = RPC._rpc_show_config(config, state)
|
strategy_version = rpc._freqtrade.strategy.version()
|
||||||
|
resp = RPC._rpc_show_config(config, state, strategy_version)
|
||||||
resp['api_version'] = API_VERSION
|
resp['api_version'] = API_VERSION
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -99,7 +99,8 @@ class RPC:
|
|||||||
self._fiat_converter = CryptoToFiatConverter()
|
self._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rpc_show_config(config, botstate: Union[State, str]) -> Dict[str, Any]:
|
def _rpc_show_config(config, botstate: Union[State, str],
|
||||||
|
strategy_version: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Return a dict of config options.
|
Return a dict of config options.
|
||||||
Explicitly does NOT return the full config to avoid leakage of sensitive
|
Explicitly does NOT return the full config to avoid leakage of sensitive
|
||||||
@ -107,6 +108,7 @@ class RPC:
|
|||||||
"""
|
"""
|
||||||
val = {
|
val = {
|
||||||
'version': __version__,
|
'version': __version__,
|
||||||
|
'strategy_version': strategy_version,
|
||||||
'dry_run': config['dry_run'],
|
'dry_run': config['dry_run'],
|
||||||
'trading_mode': config.get('trading_mode', 'spot'),
|
'trading_mode': config.get('trading_mode', 'spot'),
|
||||||
'short_allowed': config.get('trading_mode', 'spot') != 'spot',
|
'short_allowed': config.get('trading_mode', 'spot') != 'spot',
|
||||||
|
@ -1307,7 +1307,12 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._send_msg('*Version:* `{}`'.format(__version__))
|
strategy_version = self._rpc._freqtrade.strategy.version()
|
||||||
|
version_string = f'*Version:* `{__version__}`'
|
||||||
|
if strategy_version is not None:
|
||||||
|
version_string += f', *Strategy version: * `{strategy_version}`'
|
||||||
|
|
||||||
|
self._send_msg(version_string)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _show_config(self, update: Update, context: CallbackContext) -> None:
|
def _show_config(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -413,6 +413,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def version(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Returns version of the strategy.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
###
|
###
|
||||||
# END - Intended to be overridden by strategy
|
# END - Intended to be overridden by strategy
|
||||||
###
|
###
|
||||||
|
@ -113,8 +113,12 @@ class Worker:
|
|||||||
if self._heartbeat_interval:
|
if self._heartbeat_interval:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if (now - self._heartbeat_msg) > self._heartbeat_interval:
|
if (now - self._heartbeat_msg) > self._heartbeat_interval:
|
||||||
|
version = __version__
|
||||||
|
strategy_version = self.freqtrade.strategy.version()
|
||||||
|
if (strategy_version is not None):
|
||||||
|
version += ', strategy_version: ' + strategy_version
|
||||||
logger.info(f"Bot heartbeat. PID={getpid()}, "
|
logger.info(f"Bot heartbeat. PID={getpid()}, "
|
||||||
f"version='{__version__}', state='{state.name}'")
|
f"version='{version}', state='{state.name}'")
|
||||||
self._heartbeat_msg = now
|
self._heartbeat_msg = now
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
@ -2,7 +2,7 @@ numpy==1.21.4
|
|||||||
pandas==1.3.4
|
pandas==1.3.4
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.62.42
|
ccxt==1.63.1
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==36.0.0
|
cryptography==36.0.0
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
|
@ -536,6 +536,7 @@ def test_api_show_config(botclient):
|
|||||||
assert response['state'] == 'running'
|
assert response['state'] == 'running'
|
||||||
assert response['bot_name'] == 'freqtrade'
|
assert response['bot_name'] == 'freqtrade'
|
||||||
assert response['trading_mode'] == 'spot'
|
assert response['trading_mode'] == 'spot'
|
||||||
|
assert response['strategy_version'] is None
|
||||||
assert not response['trailing_stop']
|
assert not response['trailing_stop']
|
||||||
assert 'bid_strategy' in response
|
assert 'bid_strategy' in response
|
||||||
assert 'ask_strategy' in response
|
assert 'ask_strategy' in response
|
||||||
|
@ -1605,12 +1605,20 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
def test_version_handle(default_conf, update, mocker) -> None:
|
def test_version_handle(default_conf, update, mocker) -> None:
|
||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
telegram._version(update=update, context=MagicMock())
|
telegram._version(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
freqtradebot.strategy.version = lambda: '1.1.1'
|
||||||
|
|
||||||
|
telegram._version(update=update, context=MagicMock())
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_show_config_handle(default_conf, update, mocker) -> None:
|
def test_show_config_handle(default_conf, update, mocker) -> None:
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user