Merge branch 'develop' into feat_readjust_entry

This commit is contained in:
eSeR1805 2022-04-30 13:39:23 +03:00
commit 3be2afdd88
No known key found for this signature in database
GPG Key ID: BA53686259B46936
19 changed files with 97 additions and 46 deletions

View File

@ -90,7 +90,7 @@
}, },
"bot_name": "freqtrade", "bot_name": "freqtrade",
"initial_state": "running", "initial_state": "running",
"force_enter_enable": false, "force_entry_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} }

View File

@ -7,6 +7,7 @@ Depending on the callback used, they may be called when entering / exiting a tra
Currently available callbacks: Currently available callbacks:
* [`bot_start()`](#bot-start)
* [`bot_loop_start()`](#bot-loop-start) * [`bot_loop_start()`](#bot-loop-start)
* [`custom_stake_amount()`](#stake-size-management) * [`custom_stake_amount()`](#stake-size-management)
* [`custom_exit()`](#custom-exit-signal) * [`custom_exit()`](#custom-exit-signal)
@ -22,6 +23,29 @@ Currently available callbacks:
!!! Tip "Callback calling sequence" !!! Tip "Callback calling sequence"
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
## Bot start
A simple callback which is called once when the strategy is loaded.
This can be used to perform actions that must only be performed once and runs after dataprovider and wallet are set
``` python
import requests
class AwesomeStrategy(IStrategy):
# ... populate_* methods
def bot_start(self, **kwargs) -> None:
"""
Called only once after bot instantiation.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
if self.config['runmode'].value in ('live', 'dry_run'):
# Assign this to the class by using self.*
# can then be used by populate_* methods
self.cust_remote_data = requests.get('https://some_remote_source.example.com')
```
## Bot loop start ## Bot loop start
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently). A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).

View File

@ -459,8 +459,6 @@ SCHEMA_BACKTEST_REQUIRED = [
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'dry_run_wallet', 'dry_run_wallet',
'stoploss',
'minimal_roi',
'dataformat_ohlcv', 'dataformat_ohlcv',
'dataformat_trades', 'dataformat_trades',
] ]

View File

@ -12,7 +12,8 @@ import pandas as pd
from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import get_backtest_metadata_filename, json_load from freqtrade.misc import json_load
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.persistence import LocalTrade, Trade, init_db

View File

@ -123,6 +123,8 @@ class FreqtradeBot(LoggingMixin):
self._schedule.every().day.at(t).do(update) self._schedule.every().day.at(t).do(update)
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc) self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
self.strategy.bot_start()
def notify_status(self, msg: str) -> None: def notify_status(self, msg: str) -> None:
""" """
Public method for users of this class (worker, etc.) to send notifications Public method for users of this class (worker, etc.) to send notifications

View File

@ -2,13 +2,11 @@
Various tool function for Freqtrade and scripts Various tool function for Freqtrade and scripts
""" """
import gzip import gzip
import hashlib
import logging import logging
import re import re
from copy import deepcopy
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Iterator, List, Union from typing import Any, Iterator, List
from typing.io import IO from typing.io import IO
from urllib.parse import urlparse from urllib.parse import urlparse
@ -251,34 +249,3 @@ def parse_db_uri_for_logging(uri: str):
return uri return uri
pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0] pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0]
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@') return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
def get_strategy_run_id(strategy) -> str:
"""
Generate unique identification hash for a backtest run. Identical config and strategy file will
always return an identical hash.
:param strategy: strategy object.
:return: hex string id.
"""
digest = hashlib.sha1()
config = deepcopy(strategy.config)
# Options that have no impact on results of individual backtest.
not_important_keys = ('strategy_list', 'original_config', 'telegram', 'api_server')
for k in not_important_keys:
if k in config:
del config[k]
# Explicitly allow NaN values (e.g. max_open_trades).
# as it does not matter for getting the hash.
digest.update(rapidjson.dumps(config, default=str,
number_mode=rapidjson.NM_NAN).encode('utf-8'))
with open(strategy.__file__, 'rb') as fp:
digest.update(fp.read())
return digest.hexdigest().lower()
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path:
"""Return metadata filename for specified backtest results file."""
filename = Path(filename)
return filename.parent / Path(f'{filename.stem}.meta{filename.suffix}')

View File

@ -0,0 +1,40 @@
import hashlib
from copy import deepcopy
from pathlib import Path
from typing import Union
import rapidjson
def get_strategy_run_id(strategy) -> str:
"""
Generate unique identification hash for a backtest run. Identical config and strategy file will
always return an identical hash.
:param strategy: strategy object.
:return: hex string id.
"""
digest = hashlib.sha1()
config = deepcopy(strategy.config)
# Options that have no impact on results of individual backtest.
not_important_keys = ('strategy_list', 'original_config', 'telegram', 'api_server')
for k in not_important_keys:
if k in config:
del config[k]
# Explicitly allow NaN values (e.g. max_open_trades).
# as it does not matter for getting the hash.
digest.update(rapidjson.dumps(config, default=str,
number_mode=rapidjson.NM_NAN).encode('utf-8'))
# Include _ft_params_from_file - so changing parameter files cause cache eviction
digest.update(rapidjson.dumps(
strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN).encode('utf-8'))
with open(strategy.__file__, 'rb') as fp:
digest.update(fp.read())
return digest.hexdigest().lower()
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path:
"""Return metadata filename for specified backtest results file."""
filename = Path(filename)
return filename.parent / Path(f'{filename.stem}.meta{filename.suffix}')

View File

@ -24,8 +24,8 @@ from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType
TradingMode) TradingMode)
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import get_strategy_run_id
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.optimize.backtest_caching import get_strategy_run_id
from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.bt_progress import BTProgress
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
store_backtest_signal_candles, store_backtest_signal_candles,
@ -187,6 +187,7 @@ class Backtesting:
# since a "perfect" stoploss-exit is assumed anyway # since a "perfect" stoploss-exit is assumed anyway
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False self.strategy.order_types['stoploss_on_exchange'] = False
self.strategy.bot_start()
def _load_protections(self, strategy: IStrategy): def _load_protections(self, strategy: IStrategy):
if self.config.get('enable_protections', False): if self.config.get('enable_protections', False):

View File

@ -44,6 +44,7 @@ class EdgeCli:
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get( self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
self.strategy.bot_start()
def start(self) -> None: def start(self) -> None:
result = self.edge.calculate(self.config['exchange']['pair_whitelist']) result = self.edge.calculate(self.config['exchange']['pair_whitelist'])

View File

@ -11,8 +11,8 @@ from tabulate import tabulate
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.btanalysis import (calculate_cagr, calculate_csum, calculate_market_change, from freqtrade.data.btanalysis import (calculate_cagr, calculate_csum, calculate_market_change,
calculate_max_drawdown) calculate_max_drawdown)
from freqtrade.misc import (decimals_per_coin, file_dump_joblib, file_dump_json, from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
get_backtest_metadata_filename, round_coin_value) from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -610,6 +610,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange) IStrategy.dp = DataProvider(config, exchange)
strategy.bot_start()
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange'] timerange = plot_elements['timerange']
trades = plot_elements['trades'] trades = plot_elements['trades']

View File

@ -2,7 +2,7 @@ import logging
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Any, Dict from typing import Any, Dict
import rapidjson import orjson
import uvicorn import uvicorn
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -24,7 +24,7 @@ class FTJSONResponse(JSONResponse):
Use rapidjson for responses Use rapidjson for responses
Handles NaN and Inf / -Inf in a javascript way by default. Handles NaN and Inf / -Inf in a javascript way by default.
""" """
return rapidjson.dumps(content).encode("utf-8") return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
class ApiServer(RPCHandler): class ApiServer(RPCHandler):

View File

@ -193,6 +193,13 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return self.populate_sell_trend(dataframe, metadata) return self.populate_sell_trend(dataframe, metadata)
def bot_start(self, **kwargs) -> None:
"""
Called only once after bot instantiation.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def bot_loop_start(self, **kwargs) -> None: def bot_loop_start(self, **kwargs) -> None:
""" """
Called at the start of the bot iteration (one loop). Called at the start of the bot iteration (one loop).

View File

@ -27,6 +27,8 @@ py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.6 python-rapidjson==1.6
# Properly format api responses
orjson==3.6.8
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2

View File

@ -57,6 +57,7 @@ setup(
'pycoingecko', 'pycoingecko',
'py_find_1st', 'py_find_1st',
'python-rapidjson', 'python-rapidjson',
'orjson',
'sdnotify', 'sdnotify',
'colorama', 'colorama',
'jinja2', 'jinja2',

View File

@ -22,7 +22,7 @@ from freqtrade.data.history import get_timerange
from freqtrade.enums import ExitType, RunMode from freqtrade.enums import ExitType, RunMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.misc import get_strategy_run_id from freqtrade.optimize.backtest_caching import get_strategy_run_id
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade from freqtrade.persistence import LocalTrade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
@ -312,6 +312,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
get_fee.assert_called() get_fee.assert_called()
assert backtesting.fee == 0.5 assert backtesting.fee == 0.5
assert not backtesting.strategy.order_types["stoploss_on_exchange"] assert not backtesting.strategy.order_types["stoploss_on_exchange"]
assert backtesting.strategy.bot_started is True
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:

View File

@ -94,6 +94,7 @@ def test_edge_init(mocker, edge_conf) -> None:
assert edge_cli.config == edge_conf assert edge_cli.config == edge_conf
assert edge_cli.config['stake_amount'] == 'unlimited' assert edge_cli.config['stake_amount'] == 'unlimited'
assert callable(edge_cli.edge.calculate) assert callable(edge_cli.edge.calculate)
assert edge_cli.strategy.bot_started is True
def test_edge_init_fee(mocker, edge_conf) -> None: def test_edge_init_fee(mocker, edge_conf) -> None:

View File

@ -13,7 +13,6 @@ import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from numpy import isnan
from requests.auth import _basic_auth_str from requests.auth import _basic_auth_str
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
@ -985,7 +984,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
assert_response(rc) assert_response(rc)
resp_values = rc.json() resp_values = rc.json()
assert len(resp_values) == 4 assert len(resp_values) == 4
assert isnan(resp_values[0]['profit_abs']) assert resp_values[0]['profit_abs'] is None
def test_api_version(botclient): def test_api_version(botclient):

View File

@ -82,6 +82,11 @@ class StrategyTestV3(IStrategy):
# }) # })
# return prot # return prot
bot_started = False
def bot_start(self):
self.bot_started = True
def informative_pairs(self): def informative_pairs(self):
return [] return []