Merge branch 'develop' into feat_readjust_entry
This commit is contained in:
commit
3be2afdd88
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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).
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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}')
|
|
||||||
|
40
freqtrade/optimize/backtest_caching.py
Normal file
40
freqtrade/optimize/backtest_caching.py
Normal 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}')
|
@ -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):
|
||||||
|
@ -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'])
|
||||||
|
@ -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__)
|
||||||
|
@ -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']
|
||||||
|
@ -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):
|
||||||
|
@ -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).
|
||||||
|
@ -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
|
||||||
|
1
setup.py
1
setup.py
@ -57,6 +57,7 @@ setup(
|
|||||||
'pycoingecko',
|
'pycoingecko',
|
||||||
'py_find_1st',
|
'py_find_1st',
|
||||||
'python-rapidjson',
|
'python-rapidjson',
|
||||||
|
'orjson',
|
||||||
'sdnotify',
|
'sdnotify',
|
||||||
'colorama',
|
'colorama',
|
||||||
'jinja2',
|
'jinja2',
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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 []
|
||||||
|
Loading…
Reference in New Issue
Block a user