Merge branch 'freqtrade:develop' into develop
This commit is contained in:
@@ -22,5 +22,6 @@ from freqtrade.commands.optimize_commands import (start_backtesting, start_backt
|
||||
start_edge, start_hyperopt)
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
||||
from freqtrade.commands.trade_commands import start_trading
|
||||
from freqtrade.commands.webserver_commands import start_webserver
|
||||
|
@@ -40,8 +40,8 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
|
||||
|
||||
if (not Path(signals_file).exists()):
|
||||
raise OperationalException(
|
||||
(f"Cannot find latest backtest signals file: {signals_file}."
|
||||
"Run backtesting with `--export signals`.")
|
||||
f"Cannot find latest backtest signals file: {signals_file}."
|
||||
"Run backtesting with `--export signals`."
|
||||
)
|
||||
|
||||
return config
|
||||
|
@@ -111,10 +111,13 @@ ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
||||
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv",
|
||||
"strategy-updater"]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
|
||||
ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||
|
||||
|
||||
class Arguments:
|
||||
"""
|
||||
@@ -198,8 +201,8 @@ class Arguments:
|
||||
start_list_freqAI_models, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
||||
start_trading, start_webserver)
|
||||
start_plot_profit, start_show_trades, start_strategy_update,
|
||||
start_test_pairlist, start_trading, start_webserver)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
# Use custom message when no subhandler is added
|
||||
@@ -440,3 +443,11 @@ class Arguments:
|
||||
parents=[_common_parser])
|
||||
webserver_cmd.set_defaults(func=start_webserver)
|
||||
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
||||
|
||||
# Add strategy_updater subcommand
|
||||
strategy_updater_cmd = subparsers.add_parser('strategy-updater',
|
||||
help='updates outdated strategy'
|
||||
'files to the current version',
|
||||
parents=[_common_parser])
|
||||
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
||||
self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd)
|
||||
|
@@ -204,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
||||
pair, timeframe, candle_type,
|
||||
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
||||
) for pair, timeframe, candle_type in paircombs]
|
||||
|
||||
print(tabulate([
|
||||
(pair, timeframe, candle_type,
|
||||
start.strftime(DATETIME_PRINT_FORMAT),
|
||||
end.strftime(DATETIME_PRINT_FORMAT))
|
||||
for pair, timeframe, candle_type, start, end in paircombs1
|
||||
for pair, timeframe, candle_type, start, end in sorted(
|
||||
paircombs1,
|
||||
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
||||
],
|
||||
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
||||
tablefmt='psql', stralign='right'))
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
@@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
init_db(config['db_url'])
|
||||
session_target = Trade._session
|
||||
session_target = Trade.session
|
||||
init_db(config['db_url_from'])
|
||||
logger.info("Starting db migration.")
|
||||
|
||||
@@ -36,16 +36,16 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
||||
|
||||
session_target.commit()
|
||||
|
||||
for pairlock in PairLock.query:
|
||||
for pairlock in PairLock.get_all_locks():
|
||||
pairlock_count += 1
|
||||
make_transient(pairlock)
|
||||
session_target.add(pairlock)
|
||||
session_target.commit()
|
||||
|
||||
# Update sequences
|
||||
max_trade_id = session_target.query(func.max(Trade.id)).scalar()
|
||||
max_order_id = session_target.query(func.max(Order.id)).scalar()
|
||||
max_pairlock_id = session_target.query(func.max(PairLock.id)).scalar()
|
||||
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
||||
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
||||
|
||||
set_sequence_ids(session_target.get_bind(),
|
||||
trade_id=max_trade_id,
|
||||
|
55
freqtrade/commands/strategy_utils_commands.py
Normal file
55
freqtrade/commands/strategy_utils_commands.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.strategyupdater import StrategyUpdater
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_strategy_update(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start the strategy updating script
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if sys.version_info == (3, 8): # pragma: no cover
|
||||
sys.exit("Freqtrade strategy updater requires Python version >= 3.9")
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
strategy_objs = StrategyResolver.search_all_objects(
|
||||
config, enum_failed=False, recursive=config.get('recursive_strategy_search', False))
|
||||
|
||||
filtered_strategy_objs = []
|
||||
if args['strategy_list']:
|
||||
filtered_strategy_objs = [
|
||||
strategy_obj for strategy_obj in strategy_objs
|
||||
if strategy_obj['name'] in args['strategy_list']
|
||||
]
|
||||
|
||||
else:
|
||||
# Use all available entries.
|
||||
filtered_strategy_objs = strategy_objs
|
||||
|
||||
processed_locations = set()
|
||||
for strategy_obj in filtered_strategy_objs:
|
||||
if strategy_obj['location'] not in processed_locations:
|
||||
processed_locations.add(strategy_obj['location'])
|
||||
start_conversion(strategy_obj, config)
|
||||
|
||||
|
||||
def start_conversion(strategy_obj, config):
|
||||
print(f"Conversion of {Path(strategy_obj['location']).name} started.")
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
start = time.perf_counter()
|
||||
instance_strategy_updater.start(config, strategy_obj)
|
||||
elapsed = time.perf_counter() - start
|
||||
print(f"Conversion of {Path(strategy_obj['location']).name} took {elapsed:.1f} seconds.")
|
@@ -27,10 +27,7 @@ def _extend_validator(validator_class):
|
||||
if 'default' in subschema:
|
||||
instance.setdefault(prop, subschema['default'])
|
||||
|
||||
for error in validate_properties(
|
||||
validator, properties, instance, schema,
|
||||
):
|
||||
yield error
|
||||
yield from validate_properties(validator, properties, instance, schema)
|
||||
|
||||
return validators.extend(
|
||||
validator_class, {'properties': set_defaults}
|
||||
|
@@ -36,9 +36,10 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5']
|
||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet']
|
||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||
BACKTEST_CACHE_DEFAULT = 'day'
|
||||
|
@@ -373,7 +373,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
|
||||
filters = []
|
||||
if strategy:
|
||||
filters.append(Trade.strategy == strategy)
|
||||
trades = trade_list_to_dataframe(Trade.get_trades(filters).all())
|
||||
trades = trade_list_to_dataframe(list(Trade.get_trades(filters).all()))
|
||||
|
||||
return trades
|
||||
|
||||
|
@@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||
from freqtrade.exchange.types import OrderBook
|
||||
from freqtrade.misc import append_candles_to_dataframe
|
||||
from freqtrade.rpc import RPCManager
|
||||
from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
|
||||
from freqtrade.util import PeriodicCache
|
||||
|
||||
|
||||
@@ -118,8 +119,7 @@ class DataProvider:
|
||||
:param new_candle: This is a new candle
|
||||
"""
|
||||
if self.__rpc:
|
||||
self.__rpc.send_msg(
|
||||
{
|
||||
msg: RPCAnalyzedDFMsg = {
|
||||
'type': RPCMessageType.ANALYZED_DF,
|
||||
'data': {
|
||||
'key': pair_key,
|
||||
@@ -127,7 +127,7 @@ class DataProvider:
|
||||
'la': datetime.now(timezone.utc)
|
||||
}
|
||||
}
|
||||
)
|
||||
self.__rpc.send_msg(msg)
|
||||
if new_candle:
|
||||
self.__rpc.send_msg({
|
||||
'type': RPCMessageType.NEW_CANDLE,
|
||||
|
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
from pandas import DataFrame, read_feather, to_datetime
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
from .idatahandler import IDataHandler
|
||||
@@ -92,12 +92,11 @@ class FeatherDataHandler(IDataHandler):
|
||||
:param data: List of Lists containing trade data,
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
self.create_dir_if_needed(filename)
|
||||
|
||||
raise NotImplementedError()
|
||||
# array = pa.array(data)
|
||||
# array
|
||||
# feather.write_feather(data, filename)
|
||||
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
|
||||
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
"""
|
||||
@@ -116,14 +115,13 @@ class FeatherDataHandler(IDataHandler):
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
||||
# tradesdata = misc.file_load_json(filename)
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
if not filename.exists():
|
||||
return []
|
||||
|
||||
# if not tradesdata:
|
||||
# return []
|
||||
tradesdata = read_feather(filename)
|
||||
|
||||
# return tradesdata
|
||||
return tradesdata.values.tolist()
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
|
@@ -23,7 +23,7 @@ class Binance(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||
"order_time_in_force": ['GTC', 'FOK', 'IOC'],
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
@@ -31,6 +31,7 @@ class Binance(Exchange):
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||
"tickers_have_price": False,
|
||||
"floor_leverage": True,
|
||||
"stop_price_type_field": "workingType",
|
||||
|
@@ -114,7 +114,7 @@ class Bybit(Exchange):
|
||||
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||
return data
|
||||
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
params = {'leverage': leverage}
|
||||
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
|
||||
|
@@ -60,7 +60,6 @@ class Exchange:
|
||||
_ft_has_default: Dict = {
|
||||
"stoploss_on_exchange": False,
|
||||
"order_time_in_force": ["GTC"],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||
@@ -206,6 +205,8 @@ class Exchange:
|
||||
and self._api_async.session):
|
||||
logger.debug("Closing async ccxt session.")
|
||||
self.loop.run_until_complete(self._api_async.close())
|
||||
if self.loop and not self.loop.is_closed():
|
||||
self.loop.close()
|
||||
|
||||
def validate_config(self, config):
|
||||
# Check if timeframe is available
|
||||
@@ -1019,10 +1020,10 @@ class Exchange:
|
||||
|
||||
# Order handling
|
||||
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
self.set_margin_mode(pair, self.margin_mode)
|
||||
self._set_leverage(leverage, pair)
|
||||
self.set_margin_mode(pair, self.margin_mode, accept_fail)
|
||||
self._set_leverage(leverage, pair, accept_fail)
|
||||
|
||||
def _get_params(
|
||||
self,
|
||||
@@ -1034,8 +1035,7 @@ class Exchange:
|
||||
) -> Dict:
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'GTC' and ordertype != 'market':
|
||||
param = self._ft_has.get('time_in_force_parameter', '')
|
||||
params.update({param: time_in_force.upper()})
|
||||
params.update({'timeInForce': time_in_force.upper()})
|
||||
if reduceOnly:
|
||||
params.update({'reduceOnly': True})
|
||||
return params
|
||||
@@ -1137,7 +1137,11 @@ class Exchange:
|
||||
"sell" else (stop_price >= limit_rate))
|
||||
# Ensure rate is less than stop price
|
||||
if bad_stop_price:
|
||||
raise OperationalException(
|
||||
# This can for example happen if the stop / liquidation price is set to 0
|
||||
# Which is possible if a market-order closes right away.
|
||||
# The InvalidOrderException will bubble up to exit_positions, where it will be
|
||||
# handled gracefully.
|
||||
raise InvalidOrderException(
|
||||
"In stoploss limit order, stop price should be more than limit price. "
|
||||
f"Stop price: {stop_price}, Limit price: {limit_rate}, "
|
||||
f"Limit Price pct: {limit_price_pct}"
|
||||
@@ -1204,7 +1208,7 @@ class Exchange:
|
||||
|
||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||
|
||||
self._lev_prep(pair, leverage, side)
|
||||
self._lev_prep(pair, leverage, side, accept_fail=True)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||
amount=amount, price=limit_rate, params=params)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
@@ -2529,7 +2533,6 @@ class Exchange:
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None,
|
||||
accept_fail: bool = False,
|
||||
):
|
||||
"""
|
||||
@@ -2547,7 +2550,7 @@ class Exchange:
|
||||
self._log_exchange_response('set_leverage', res)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except ccxt.BadRequest as e:
|
||||
except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
|
||||
if not accept_fail:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
|
@@ -75,8 +75,7 @@ class Gate(Exchange):
|
||||
)
|
||||
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
|
||||
params['type'] = 'market'
|
||||
param = self._ft_has.get('time_in_force_parameter', '')
|
||||
params.update({param: 'IOC'})
|
||||
params.update({'timeInForce': 'IOC'})
|
||||
return params
|
||||
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||
|
@@ -158,7 +158,6 @@ class Kraken(Exchange):
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None,
|
||||
accept_fail: bool = False,
|
||||
):
|
||||
"""
|
||||
|
@@ -1,14 +1,16 @@
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.enums.pricetype import PriceType
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange import Exchange, date_minus_candles
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,11 +26,13 @@ class Okx(Exchange):
|
||||
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"tickers_have_quoteVolume": False,
|
||||
"fee_cost_in_contracts": True,
|
||||
"stop_price_type_field": "tpTriggerPxType",
|
||||
"stop_price_type_field": "slTriggerPxType",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "last",
|
||||
PriceType.MARK: "index",
|
||||
@@ -121,10 +125,9 @@ class Okx(Exchange):
|
||||
return params
|
||||
|
||||
@retrier
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
||||
try:
|
||||
# TODO-lev: Test me properly (check mgnMode passed)
|
||||
res = self._api.set_leverage(
|
||||
leverage=leverage,
|
||||
symbol=pair,
|
||||
@@ -157,3 +160,78 @@ class Okx(Exchange):
|
||||
|
||||
pair_tiers = self._leverage_tiers[pair]
|
||||
return pair_tiers[-1]['maxNotional'] / leverage
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopLossPrice': stop_price})
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
||||
params['tdMode'] = self.margin_mode.value
|
||||
params['posSide'] = self._get_posSide(side, True)
|
||||
return params
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
OKX uses non-default stoploss price naming.
|
||||
"""
|
||||
if not self._ft_has.get('stoploss_on_exchange'):
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
return (
|
||||
order.get('stopLossPrice', None) is None
|
||||
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
|
||||
)
|
||||
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
|
||||
try:
|
||||
params1 = {'stop': True}
|
||||
order_reg = self._api.fetch_order(order_id, pair, params=params1)
|
||||
self._log_exchange_response('fetch_stoploss_order', order_reg)
|
||||
return order_reg
|
||||
except ccxt.OrderNotFound:
|
||||
pass
|
||||
params2 = {'stop': True, 'ordType': 'conditional'}
|
||||
for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
|
||||
self._api.fetch_canceled_orders):
|
||||
try:
|
||||
orders = method(pair, params=params2)
|
||||
orders_f = [order for order in orders if order['id'] == order_id]
|
||||
if orders_f:
|
||||
order = orders_f[0]
|
||||
if (order['status'] == 'closed'
|
||||
and (real_order_id := order.get('info', {}).get('ordId')) is not None):
|
||||
# Once a order triggered, we fetch the regular followup order.
|
||||
order_reg = self.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order_reg)
|
||||
order_reg['id_stop'] = order_reg['id']
|
||||
order_reg['id'] = order_id
|
||||
order_reg['type'] = 'stoploss'
|
||||
order_reg['status_stop'] = 'triggered'
|
||||
return order_reg
|
||||
order['type'] = 'stoploss'
|
||||
return order
|
||||
except ccxt.BaseError:
|
||||
pass
|
||||
raise RetryableOrderError(
|
||||
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
|
||||
|
||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||
if order['type'] == 'stop':
|
||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||
return order['id']
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
params1 = {'stop': True}
|
||||
# 'ordType': 'conditional'
|
||||
#
|
||||
return self.cancel_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
params=params1,
|
||||
)
|
||||
|
@@ -47,7 +47,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
||||
self._update_unrealized_total_profit()
|
||||
step_reward = self.calculate_reward(action)
|
||||
self.total_reward += step_reward
|
||||
self.tensorboard_log(self.actions._member_names_[action])
|
||||
self.tensorboard_log(self.actions._member_names_[action], category="actions")
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
|
@@ -48,7 +48,7 @@ class Base4ActionRLEnv(BaseEnvironment):
|
||||
self._update_unrealized_total_profit()
|
||||
step_reward = self.calculate_reward(action)
|
||||
self.total_reward += step_reward
|
||||
self.tensorboard_log(self.actions._member_names_[action])
|
||||
self.tensorboard_log(self.actions._member_names_[action], category="actions")
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
|
@@ -49,7 +49,7 @@ class Base5ActionRLEnv(BaseEnvironment):
|
||||
self._update_unrealized_total_profit()
|
||||
step_reward = self.calculate_reward(action)
|
||||
self.total_reward += step_reward
|
||||
self.tensorboard_log(self.actions._member_names_[action])
|
||||
self.tensorboard_log(self.actions._member_names_[action], category="actions")
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
|
@@ -137,7 +137,8 @@ class BaseEnvironment(gym.Env):
|
||||
self.np_random, seed = seeding.np_random(seed)
|
||||
return [seed]
|
||||
|
||||
def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True):
|
||||
def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None,
|
||||
inc: Optional[bool] = None, category: str = "custom"):
|
||||
"""
|
||||
Function builds the tensorboard_metrics dictionary
|
||||
to be parsed by the TensorboardCallback. This
|
||||
@@ -149,17 +150,24 @@ class BaseEnvironment(gym.Env):
|
||||
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
if not self._is_valid(action):
|
||||
self.tensorboard_log("is_valid")
|
||||
self.tensorboard_log("invalid")
|
||||
return -2
|
||||
|
||||
:param metric: metric to be tracked and incremented
|
||||
:param value: value to increment `metric` by
|
||||
:param inc: sets whether the `value` is incremented or not
|
||||
:param value: `metric` value
|
||||
:param inc: (deprecated) sets whether the `value` is incremented or not
|
||||
:param category: `metric` category
|
||||
"""
|
||||
if not inc or metric not in self.tensorboard_metrics:
|
||||
self.tensorboard_metrics[metric] = value
|
||||
increment = True if value is None else False
|
||||
value = 1 if increment else value
|
||||
|
||||
if category not in self.tensorboard_metrics:
|
||||
self.tensorboard_metrics[category] = {}
|
||||
|
||||
if not increment or metric not in self.tensorboard_metrics[category]:
|
||||
self.tensorboard_metrics[category][metric] = value
|
||||
else:
|
||||
self.tensorboard_metrics[metric] += value
|
||||
self.tensorboard_metrics[category][metric] += value
|
||||
|
||||
def reset_tensorboard_log(self):
|
||||
self.tensorboard_metrics = {}
|
||||
|
@@ -13,7 +13,7 @@ class TensorboardCallback(BaseCallback):
|
||||
episodic summary reports.
|
||||
"""
|
||||
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
|
||||
super(TensorboardCallback, self).__init__(verbose)
|
||||
super().__init__(verbose)
|
||||
self.model: Any = None
|
||||
self.logger = None # type: Any
|
||||
self.training_env: BaseEnvironment = None # type: ignore
|
||||
@@ -46,14 +46,12 @@ class TensorboardCallback(BaseCallback):
|
||||
local_info = self.locals["infos"][0]
|
||||
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
|
||||
|
||||
for info in local_info:
|
||||
if info not in ["episode", "terminal_observation"]:
|
||||
self.logger.record(f"_info/{info}", local_info[info])
|
||||
for metric in local_info:
|
||||
if metric not in ["episode", "terminal_observation"]:
|
||||
self.logger.record(f"info/{metric}", local_info[metric])
|
||||
|
||||
for info in tensorboard_metrics:
|
||||
if info in [action.name for action in self.actions]:
|
||||
self.logger.record(f"_actions/{info}", tensorboard_metrics[info])
|
||||
else:
|
||||
self.logger.record(f"_custom/{info}", tensorboard_metrics[info])
|
||||
for category in tensorboard_metrics:
|
||||
for metric in tensorboard_metrics[category]:
|
||||
self.logger.record(f"{category}/{metric}", tensorboard_metrics[category][metric])
|
||||
|
||||
return True
|
||||
|
@@ -251,7 +251,7 @@ class FreqaiDataKitchen:
|
||||
(drop_index == 0) & (drop_index_labels == 0)
|
||||
]
|
||||
logger.info(
|
||||
f"dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
||||
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
||||
f" due to NaNs in populated dataset {len(unfiltered_df)}."
|
||||
)
|
||||
if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live:
|
||||
@@ -675,7 +675,7 @@ class FreqaiDataKitchen:
|
||||
]
|
||||
|
||||
logger.info(
|
||||
f"SVM tossed {len(y_pred) - kept_points.sum()}"
|
||||
f"{self.pair}: SVM tossed {len(y_pred) - kept_points.sum()}"
|
||||
f" test points from {len(y_pred)} total points."
|
||||
)
|
||||
|
||||
@@ -949,7 +949,7 @@ class FreqaiDataKitchen:
|
||||
|
||||
if (len(do_predict) - do_predict.sum()) > 0:
|
||||
logger.info(
|
||||
f"DI tossed {len(do_predict) - do_predict.sum()} predictions for "
|
||||
f"{self.pair}: DI tossed {len(do_predict) - do_predict.sum()} predictions for "
|
||||
"being too far from training data."
|
||||
)
|
||||
|
||||
|
@@ -104,6 +104,10 @@ class IFreqaiModel(ABC):
|
||||
self.data_provider: Optional[DataProvider] = None
|
||||
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
|
||||
self.can_short = True # overridden in start() with strategy.can_short
|
||||
self.model: Any = None
|
||||
if self.ft_params.get('principal_component_analysis', False) and self.continual_learning:
|
||||
self.ft_params.update({'principal_component_analysis': False})
|
||||
logger.warning('User tried to use PCA with continual learning. Deactivating PCA.')
|
||||
|
||||
record_params(config, self.full_path)
|
||||
|
||||
@@ -153,8 +157,7 @@ class IFreqaiModel(ABC):
|
||||
dk = self.start_backtesting(dataframe, metadata, self.dk, strategy)
|
||||
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
||||
else:
|
||||
logger.info(
|
||||
"Backtesting using historic predictions (live models)")
|
||||
logger.info("Backtesting using historic predictions (live models)")
|
||||
dk = self.start_backtesting_from_historic_predictions(
|
||||
dataframe, metadata, self.dk)
|
||||
dataframe = dk.return_dataframe
|
||||
@@ -338,13 +341,14 @@ class IFreqaiModel(ABC):
|
||||
except Exception as msg:
|
||||
logger.warning(
|
||||
f"Training {pair} raised exception {msg.__class__.__name__}. "
|
||||
f"Message: {msg}, skipping.")
|
||||
f"Message: {msg}, skipping.", exc_info=True)
|
||||
self.model = None
|
||||
|
||||
self.dd.pair_dict[pair]["trained_timestamp"] = int(
|
||||
tr_train.stopts)
|
||||
if self.plot_features:
|
||||
if self.plot_features and self.model is not None:
|
||||
plot_feature_importance(self.model, pair, dk, self.plot_features)
|
||||
if self.save_backtest_models:
|
||||
if self.save_backtest_models and self.model is not None:
|
||||
logger.info('Saving backtest model to disk.')
|
||||
self.dd.save_data(self.model, pair, dk)
|
||||
else:
|
||||
|
@@ -100,7 +100,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
||||
"""
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
self.tensorboard_log("is_valid")
|
||||
self.tensorboard_log("invalid", category="actions")
|
||||
return -2
|
||||
|
||||
pnl = self.get_unrealized_profit()
|
||||
|
@@ -30,6 +30,8 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.rpc import RPCManager
|
||||
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
||||
from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg,
|
||||
RPCSellMsg)
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.util import FtPrecise
|
||||
@@ -212,7 +214,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||
self.strategy.gather_informative_pairs())
|
||||
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
||||
current_time=datetime.now(timezone.utc))
|
||||
|
||||
self.strategy.analyze(self.active_pair_whitelist)
|
||||
|
||||
@@ -594,7 +597,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
stake_available = self.wallets.get_available_stake_amount()
|
||||
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
default_retval=None, supress_error=True)(
|
||||
trade=trade,
|
||||
current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
|
||||
current_profit=current_entry_profit, min_stake=min_entry_stake,
|
||||
@@ -810,6 +813,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
precision_mode=self.exchange.precisionMode,
|
||||
contract_size=self.exchange.get_contract_size(pair),
|
||||
)
|
||||
stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)
|
||||
trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True)
|
||||
|
||||
else:
|
||||
# This is additional buy, we reset fee_open_currency so timeout checking can work
|
||||
trade.is_open = True
|
||||
@@ -819,7 +825,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
trade.orders.append(order_obj)
|
||||
trade.recalc_trade_from_orders()
|
||||
Trade.query.session.add(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
# Updating wallets
|
||||
@@ -851,7 +857,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Reset stoploss order id.
|
||||
trade.stoploss_order_id = None
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} "
|
||||
f"for pair {trade.pair}")
|
||||
return trade
|
||||
|
||||
def get_valid_enter_price_and_stake(
|
||||
@@ -943,7 +950,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Sends rpc notification when a entry order occurred.
|
||||
"""
|
||||
msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
|
||||
open_rate = order.safe_price
|
||||
|
||||
if open_rate is None:
|
||||
@@ -954,9 +960,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||
|
||||
msg = {
|
||||
msg: RPCBuyMsg = {
|
||||
'trade_id': trade.id,
|
||||
'type': msg_type,
|
||||
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
@@ -968,6 +974,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'order_type': order_type,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
|
||||
'open_date': trade.open_date or datetime.utcnow(),
|
||||
@@ -986,7 +993,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||
|
||||
msg = {
|
||||
msg: RPCCancelMsg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.ENTRY_CANCEL,
|
||||
'buy_tag': trade.enter_tag,
|
||||
@@ -998,7 +1005,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
'limit': trade.open_rate,
|
||||
'order_type': order_type,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'open_rate': trade.open_rate,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'open_date': trade.open_date,
|
||||
@@ -1021,12 +1030,16 @@ class FreqtradeBot(LoggingMixin):
|
||||
trades_closed = 0
|
||||
for trade in trades:
|
||||
try:
|
||||
try:
|
||||
if (self.strategy.order_types.get('stoploss_on_exchange') and
|
||||
self.handle_stoploss_on_exchange(trade)):
|
||||
trades_closed += 1
|
||||
Trade.commit()
|
||||
continue
|
||||
|
||||
if (self.strategy.order_types.get('stoploss_on_exchange') and
|
||||
self.handle_stoploss_on_exchange(trade)):
|
||||
trades_closed += 1
|
||||
Trade.commit()
|
||||
continue
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning(
|
||||
f'Unable to handle stoploss on exchange for {trade.pair}: {exception}')
|
||||
# Check if we can sell our current pair
|
||||
if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
|
||||
trades_closed += 1
|
||||
@@ -1232,13 +1245,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
# cancelling the current stoploss on exchange first
|
||||
logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
|
||||
f"(orderid:{order['id']}) in order to add another one ...")
|
||||
try:
|
||||
co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
|
||||
trade.amount)
|
||||
trade.update_order(co)
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
||||
f"for pair {trade.pair}")
|
||||
|
||||
self.cancel_stoploss_on_exchange(trade)
|
||||
|
||||
# Create new stoploss order
|
||||
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
|
||||
@@ -1659,7 +1667,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount = trade.amount
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
msg: RPCSellMsg = {
|
||||
'type': (RPCMessageType.EXIT_FILL if fill
|
||||
else RPCMessageType.EXIT),
|
||||
'trade_id': trade.id,
|
||||
@@ -1685,6 +1693,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||
'sub_trade': sub_trade,
|
||||
'cumulative_profit': trade.realized_profit,
|
||||
@@ -1715,7 +1724,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
msg: RPCSellCancelMsg = {
|
||||
'type': RPCMessageType.EXIT_CANCEL,
|
||||
'trade_id': trade.id,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
@@ -1737,6 +1746,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'reason': reason,
|
||||
'sub_trade': sub_trade,
|
||||
@@ -1844,14 +1854,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
|
||||
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
||||
if prot_trig:
|
||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
||||
msg.update(prot_trig.to_json())
|
||||
msg: RPCProtectionMsg = {
|
||||
'type': RPCMessageType.PROTECTION_TRIGGER,
|
||||
'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair),
|
||||
**prot_trig.to_json() # type: ignore
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
prot_trig_glb = self.protections.global_stop(side=side)
|
||||
if prot_trig_glb:
|
||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
|
||||
msg.update(prot_trig_glb.to_json())
|
||||
msg = {
|
||||
'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
|
||||
'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair),
|
||||
**prot_trig_glb.to_json() # type: ignore
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
||||
|
@@ -203,9 +203,10 @@ class Backtesting:
|
||||
# since a "perfect" stoploss-exit is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
# Update can_short flag
|
||||
self._can_short = self.trading_mode != TradingMode.SPOT and strategy.can_short
|
||||
|
||||
self.strategy.ft_bot_start()
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||
|
||||
def _load_protections(self, strategy: IStrategy):
|
||||
if self.config.get('enable_protections', False):
|
||||
@@ -442,10 +443,6 @@ class Backtesting:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(
|
||||
(trade.stop_loss_pct or 0.0) / leverage))
|
||||
if is_short:
|
||||
assert stop_rate > row[LOW_IDX]
|
||||
else:
|
||||
assert stop_rate < row[HIGH_IDX]
|
||||
|
||||
# Limit lower-end to candle low to avoid exits below the low.
|
||||
# This still remains "worst case" - but "worst realistic case".
|
||||
@@ -526,7 +523,7 @@ class Backtesting:
|
||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||
stake_available = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
default_retval=None, supress_error=True)(
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=current_date, current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake,
|
||||
@@ -744,7 +741,7 @@ class Backtesting:
|
||||
proposed_leverage=1.0,
|
||||
max_leverage=max_leverage,
|
||||
side=direction, entry_tag=entry_tag,
|
||||
) if self._can_short else 1.0
|
||||
) if self.trading_mode != TradingMode.SPOT else 1.0
|
||||
# Cap leverage between 1.0 and max_leverage.
|
||||
leverage = min(max(leverage, 1.0), max_leverage)
|
||||
|
||||
@@ -1034,6 +1031,9 @@ class Backtesting:
|
||||
requested_stake=(
|
||||
order.safe_remaining * order.ft_price / trade.leverage),
|
||||
direction='short' if trade.is_short else 'long')
|
||||
# Delete trade if no successful entries happened (if placing the new order failed)
|
||||
if trade.open_order_id is None and trade.nr_of_successful_entries == 0:
|
||||
return True
|
||||
self.replaced_entry_orders += 1
|
||||
else:
|
||||
# assumption: there can't be multiple open entry orders at any given time
|
||||
@@ -1159,6 +1159,8 @@ class Backtesting:
|
||||
while current_time <= end_date:
|
||||
open_trade_count_start = LocalTrade.bt_open_open_trade_count
|
||||
self.check_abort()
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
||||
current_time=current_time)
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
row = self.validate_row(data, pair, row_index, current_time)
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import io
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
@@ -464,8 +463,8 @@ class HyperoptTools():
|
||||
return
|
||||
|
||||
try:
|
||||
io.open(csv_file, 'w+').close()
|
||||
except IOError:
|
||||
Path(csv_file).open('w+').close()
|
||||
except OSError:
|
||||
logger.error(f"Failed to create CSV file: {csv_file}")
|
||||
return
|
||||
|
||||
|
@@ -2,7 +2,9 @@
|
||||
This module contains the class to persist trades into SQLite
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
import threading
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Dict, Final, Optional
|
||||
|
||||
from sqlalchemy import create_engine, inspect
|
||||
from sqlalchemy.exc import NoSuchModuleError
|
||||
@@ -19,6 +21,22 @@ from freqtrade.persistence.trade_model import Order, Trade
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUEST_ID_CTX_KEY: Final[str] = 'request_id'
|
||||
_request_id_ctx_var: ContextVar[Optional[str]] = ContextVar(REQUEST_ID_CTX_KEY, default=None)
|
||||
|
||||
|
||||
def get_request_or_thread_id() -> Optional[str]:
|
||||
"""
|
||||
Helper method to get either async context (for fastapi requests), or thread id
|
||||
"""
|
||||
id = _request_id_ctx_var.get()
|
||||
if id is None:
|
||||
# when not in request context - use thread id
|
||||
id = str(threading.current_thread().ident)
|
||||
|
||||
return id
|
||||
|
||||
|
||||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
|
||||
|
||||
@@ -53,13 +71,11 @@ def init_db(db_url: str) -> None:
|
||||
|
||||
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
|
||||
# Scoped sessions proxy requests to the appropriate thread-local session.
|
||||
# We should use the scoped_session object - not a seperately initialized version
|
||||
Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=False))
|
||||
Order._session = Trade._session
|
||||
PairLock._session = Trade._session
|
||||
Trade.query = Trade._session.query_property()
|
||||
Order.query = Trade._session.query_property()
|
||||
PairLock.query = Trade._session.query_property()
|
||||
# Since we also use fastAPI, we need to make it aware of the request id, too
|
||||
Trade.session = scoped_session(sessionmaker(
|
||||
bind=engine, autoflush=False), scopefunc=get_request_or_thread_id)
|
||||
Order.session = Trade.session
|
||||
PairLock.session = Trade.session
|
||||
|
||||
previous_tables = inspect(engine).get_table_names()
|
||||
ModelBase.metadata.create_all(engine)
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, ClassVar, Dict, Optional
|
||||
|
||||
from sqlalchemy import String, or_
|
||||
from sqlalchemy.orm import Mapped, Query, QueryPropertyDescriptor, mapped_column
|
||||
from sqlalchemy import ScalarResult, String, or_, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.persistence.base import ModelBase, SessionType
|
||||
@@ -13,8 +13,7 @@ class PairLock(ModelBase):
|
||||
Pair Locks database model.
|
||||
"""
|
||||
__tablename__ = 'pairlocks'
|
||||
query: ClassVar[QueryPropertyDescriptor]
|
||||
_session: ClassVar[SessionType]
|
||||
session: ClassVar[SessionType]
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
|
||||
@@ -37,7 +36,8 @@ class PairLock(ModelBase):
|
||||
f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})')
|
||||
|
||||
@staticmethod
|
||||
def query_pair_locks(pair: Optional[str], now: datetime, side: str = '*') -> Query:
|
||||
def query_pair_locks(
|
||||
pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']:
|
||||
"""
|
||||
Get all currently active locks for this pair
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
@@ -53,9 +53,11 @@ class PairLock(ModelBase):
|
||||
else:
|
||||
filters.append(PairLock.side == '*')
|
||||
|
||||
return PairLock.query.filter(
|
||||
*filters
|
||||
)
|
||||
return PairLock.session.scalars(select(PairLock).filter(*filters))
|
||||
|
||||
@staticmethod
|
||||
def get_all_locks() -> ScalarResult['PairLock']:
|
||||
return PairLock.session.scalars(select(PairLock))
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
return {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.exchange import timeframe_to_next_date
|
||||
from freqtrade.persistence.models import PairLock
|
||||
@@ -51,15 +53,15 @@ class PairLocks():
|
||||
active=True
|
||||
)
|
||||
if PairLocks.use_db:
|
||||
PairLock.query.session.add(lock)
|
||||
PairLock.query.session.commit()
|
||||
PairLock.session.add(lock)
|
||||
PairLock.session.commit()
|
||||
else:
|
||||
PairLocks.locks.append(lock)
|
||||
return lock
|
||||
|
||||
@staticmethod
|
||||
def get_pair_locks(
|
||||
pair: Optional[str], now: Optional[datetime] = None, side: str = '*') -> List[PairLock]:
|
||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None,
|
||||
side: str = '*') -> Sequence[PairLock]:
|
||||
"""
|
||||
Get all currently active locks for this pair
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
@@ -106,7 +108,7 @@ class PairLocks():
|
||||
for lock in locks:
|
||||
lock.active = False
|
||||
if PairLocks.use_db:
|
||||
PairLock.query.session.commit()
|
||||
PairLock.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def unlock_reason(reason: str, now: Optional[datetime] = None) -> None:
|
||||
@@ -126,11 +128,11 @@ class PairLocks():
|
||||
PairLock.active.is_(True),
|
||||
PairLock.reason == reason
|
||||
]
|
||||
locks = PairLock.query.filter(*filters)
|
||||
locks = PairLock.session.scalars(select(PairLock).filter(*filters)).all()
|
||||
for lock in locks:
|
||||
logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.")
|
||||
lock.active = False
|
||||
PairLock.query.session.commit()
|
||||
PairLock.session.commit()
|
||||
else:
|
||||
# used in backtesting mode; don't show log messages for speed
|
||||
locksb = PairLocks.get_pair_locks(None)
|
||||
@@ -165,11 +167,11 @@ class PairLocks():
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_all_locks() -> List[PairLock]:
|
||||
def get_all_locks() -> Sequence[PairLock]:
|
||||
"""
|
||||
Return all locks, also locks with expired end date
|
||||
"""
|
||||
if PairLocks.use_db:
|
||||
return PairLock.query.all()
|
||||
return PairLock.get_all_locks().all()
|
||||
else:
|
||||
return PairLocks.locks
|
||||
|
@@ -5,11 +5,11 @@ import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import isclose
|
||||
from typing import Any, ClassVar, Dict, List, Optional, cast
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast
|
||||
|
||||
from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func
|
||||
from sqlalchemy.orm import (Mapped, Query, QueryPropertyDescriptor, lazyload, mapped_column,
|
||||
relationship)
|
||||
from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String,
|
||||
UniqueConstraint, desc, func, select)
|
||||
from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship
|
||||
|
||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
|
||||
BuySell, LongShort)
|
||||
@@ -36,8 +36,7 @@ class Order(ModelBase):
|
||||
Mirrors CCXT Order structure
|
||||
"""
|
||||
__tablename__ = 'orders'
|
||||
query: ClassVar[QueryPropertyDescriptor]
|
||||
_session: ClassVar[SessionType]
|
||||
session: ClassVar[SessionType]
|
||||
|
||||
# Uniqueness should be ensured over pair, order_id
|
||||
# its likely that order_id is unique per Pair on some exchanges.
|
||||
@@ -263,12 +262,12 @@ class Order(ModelBase):
|
||||
return o
|
||||
|
||||
@staticmethod
|
||||
def get_open_orders() -> List['Order']:
|
||||
def get_open_orders() -> Sequence['Order']:
|
||||
"""
|
||||
Retrieve open orders from the database
|
||||
:return: List of open orders
|
||||
"""
|
||||
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
||||
return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all()
|
||||
|
||||
@staticmethod
|
||||
def order_by_id(order_id: str) -> Optional['Order']:
|
||||
@@ -276,7 +275,7 @@ class Order(ModelBase):
|
||||
Retrieve order based on order_id
|
||||
:return: Order or None
|
||||
"""
|
||||
return Order.query.filter(Order.order_id == order_id).first()
|
||||
return Order.session.scalars(select(Order).filter(Order.order_id == order_id)).first()
|
||||
|
||||
|
||||
class LocalTrade():
|
||||
@@ -561,6 +560,9 @@ class LocalTrade():
|
||||
'trading_mode': self.trading_mode,
|
||||
'funding_fees': self.funding_fees,
|
||||
'open_order_id': self.open_order_id,
|
||||
'amount_precision': self.amount_precision,
|
||||
'price_precision': self.price_precision,
|
||||
'precision_mode': self.precision_mode,
|
||||
'orders': orders,
|
||||
}
|
||||
|
||||
@@ -1153,7 +1155,9 @@ class LocalTrade():
|
||||
get open trade count
|
||||
"""
|
||||
if Trade.use_db:
|
||||
return Trade.query.filter(Trade.is_open.is_(True)).count()
|
||||
return Trade.session.execute(
|
||||
select(func.count(Trade.id)).filter(Trade.is_open.is_(True))
|
||||
).scalar_one()
|
||||
else:
|
||||
return LocalTrade.bt_open_open_trade_count
|
||||
|
||||
@@ -1186,8 +1190,7 @@ class Trade(ModelBase, LocalTrade):
|
||||
Note: Fields must be aligned with LocalTrade class
|
||||
"""
|
||||
__tablename__ = 'trades'
|
||||
query: ClassVar[QueryPropertyDescriptor]
|
||||
_session: ClassVar[SessionType]
|
||||
session: ClassVar[SessionType]
|
||||
|
||||
use_db: bool = True
|
||||
|
||||
@@ -1287,18 +1290,18 @@ class Trade(ModelBase, LocalTrade):
|
||||
def delete(self) -> None:
|
||||
|
||||
for order in self.orders:
|
||||
Order.query.session.delete(order)
|
||||
Order.session.delete(order)
|
||||
|
||||
Trade.query.session.delete(self)
|
||||
Trade.session.delete(self)
|
||||
Trade.commit()
|
||||
|
||||
@staticmethod
|
||||
def commit():
|
||||
Trade.query.session.commit()
|
||||
Trade.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def rollback():
|
||||
Trade.query.session.rollback()
|
||||
Trade.session.rollback()
|
||||
|
||||
@staticmethod
|
||||
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
|
||||
@@ -1332,7 +1335,7 @@ class Trade(ModelBase, LocalTrade):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_trades(trade_filter=None, include_orders: bool = True) -> Query['Trade']:
|
||||
def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select:
|
||||
"""
|
||||
Helper function to query Trades using filters.
|
||||
NOTE: Not supported in Backtesting.
|
||||
@@ -1347,22 +1350,35 @@ class Trade(ModelBase, LocalTrade):
|
||||
if trade_filter is not None:
|
||||
if not isinstance(trade_filter, list):
|
||||
trade_filter = [trade_filter]
|
||||
this_query = Trade.query.filter(*trade_filter)
|
||||
this_query = select(Trade).filter(*trade_filter)
|
||||
else:
|
||||
this_query = Trade.query
|
||||
this_query = select(Trade)
|
||||
if not include_orders:
|
||||
# Don't load order relations
|
||||
# Consider using noload or raiseload instead of lazyload
|
||||
this_query = this_query.options(lazyload(Trade.orders))
|
||||
return this_query
|
||||
|
||||
@staticmethod
|
||||
def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult['Trade']:
|
||||
"""
|
||||
Helper function to query Trades using filters.
|
||||
NOTE: Not supported in Backtesting.
|
||||
:param trade_filter: Optional filter to apply to trades
|
||||
Can be either a Filter object, or a List of filters
|
||||
e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
|
||||
e.g. `(trade_filter=Trade.id == trade_id)`
|
||||
:return: unsorted query object
|
||||
"""
|
||||
return Trade.session.scalars(Trade.get_trades_query(trade_filter, include_orders))
|
||||
|
||||
@staticmethod
|
||||
def get_open_order_trades() -> List['Trade']:
|
||||
"""
|
||||
Returns all open trades
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
|
||||
return cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all())
|
||||
|
||||
@staticmethod
|
||||
def get_open_trades_without_assigned_fees():
|
||||
@@ -1392,11 +1408,12 @@ class Trade(ModelBase, LocalTrade):
|
||||
Retrieves total realized profit
|
||||
"""
|
||||
if Trade.use_db:
|
||||
total_profit = Trade.query.with_entities(
|
||||
func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)).scalar()
|
||||
total_profit: float = Trade.session.execute(
|
||||
select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False))
|
||||
).scalar_one()
|
||||
else:
|
||||
total_profit = sum(
|
||||
t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False))
|
||||
total_profit = sum(t.close_profit_abs # type: ignore
|
||||
for t in LocalTrade.get_trades_proxy(is_open=False))
|
||||
return total_profit or 0
|
||||
|
||||
@staticmethod
|
||||
@@ -1406,8 +1423,9 @@ class Trade(ModelBase, LocalTrade):
|
||||
in stake currency
|
||||
"""
|
||||
if Trade.use_db:
|
||||
total_open_stake_amount = Trade.query.with_entities(
|
||||
func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
|
||||
total_open_stake_amount = Trade.session.scalar(
|
||||
select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True))
|
||||
)
|
||||
else:
|
||||
total_open_stake_amount = sum(
|
||||
t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
|
||||
@@ -1423,15 +1441,18 @@ class Trade(ModelBase, LocalTrade):
|
||||
if minutes:
|
||||
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
|
||||
filters.append(Trade.close_date >= start_date)
|
||||
pair_rates = Trade.query.with_entities(
|
||||
Trade.pair,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
pair_rates = Trade.session.execute(
|
||||
select(
|
||||
Trade.pair,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)
|
||||
.group_by(Trade.pair)
|
||||
.order_by(desc('profit_sum_abs'))
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
'pair': pair,
|
||||
@@ -1456,15 +1477,16 @@ class Trade(ModelBase, LocalTrade):
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
enter_tag_perf = Trade.query.with_entities(
|
||||
Trade.enter_tag,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.enter_tag) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
enter_tag_perf = Trade.session.execute(
|
||||
select(
|
||||
Trade.enter_tag,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)
|
||||
.group_by(Trade.enter_tag)
|
||||
.order_by(desc('profit_sum_abs'))
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1488,16 +1510,16 @@ class Trade(ModelBase, LocalTrade):
|
||||
filters: List = [Trade.is_open.is_(False)]
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
sell_tag_perf = Trade.query.with_entities(
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.exit_reason) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
sell_tag_perf = Trade.session.execute(
|
||||
select(
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)
|
||||
.group_by(Trade.exit_reason)
|
||||
.order_by(desc('profit_sum_abs'))
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1521,18 +1543,18 @@ class Trade(ModelBase, LocalTrade):
|
||||
filters: List = [Trade.is_open.is_(False)]
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
mix_tag_perf = Trade.query.with_entities(
|
||||
Trade.id,
|
||||
Trade.enter_tag,
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.id) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
mix_tag_perf = Trade.session.execute(
|
||||
select(
|
||||
Trade.id,
|
||||
Trade.enter_tag,
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)
|
||||
.group_by(Trade.id)
|
||||
.order_by(desc('profit_sum_abs'))
|
||||
).all()
|
||||
|
||||
return_list: List[Dict] = []
|
||||
for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
@@ -1568,11 +1590,15 @@ class Trade(ModelBase, LocalTrade):
|
||||
NOTE: Not supported in Backtesting.
|
||||
:returns: Tuple containing (pair, profit_sum)
|
||||
"""
|
||||
best_pair = Trade.query.with_entities(
|
||||
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
|
||||
).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) \
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(desc('profit_sum')).first()
|
||||
best_pair = Trade.session.execute(
|
||||
select(
|
||||
Trade.pair,
|
||||
func.sum(Trade.close_profit).label('profit_sum')
|
||||
).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))
|
||||
.group_by(Trade.pair)
|
||||
.order_by(desc('profit_sum'))
|
||||
).first()
|
||||
|
||||
return best_pair
|
||||
|
||||
@staticmethod
|
||||
@@ -1582,12 +1608,13 @@ class Trade(ModelBase, LocalTrade):
|
||||
NOTE: Not supported in Backtesting.
|
||||
:returns: Tuple containing (pair, profit_sum)
|
||||
"""
|
||||
trading_volume = Order.query.with_entities(
|
||||
func.sum(Order.cost).label('volume')
|
||||
).filter(
|
||||
Order.order_filled_date >= start_date,
|
||||
Order.status == 'closed'
|
||||
).scalar()
|
||||
trading_volume = Trade.session.execute(
|
||||
select(
|
||||
func.sum(Order.cost).label('volume')
|
||||
).filter(
|
||||
Order.order_filled_date >= start_date,
|
||||
Order.status == 'closed'
|
||||
)).scalar_one()
|
||||
return trading_volume
|
||||
|
||||
@staticmethod
|
||||
@@ -1636,8 +1663,10 @@ class Trade(ModelBase, LocalTrade):
|
||||
stop_loss=data["stop_loss_abs"],
|
||||
stop_loss_pct=data["stop_loss_ratio"],
|
||||
stoploss_order_id=data["stoploss_order_id"],
|
||||
stoploss_last_update=(datetime.fromtimestamp(data["stoploss_last_update"] // 1000,
|
||||
tz=timezone.utc) if data["stoploss_last_update"] else None),
|
||||
stoploss_last_update=(
|
||||
datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
|
||||
tz=timezone.utc)
|
||||
if data["stoploss_last_update_timestamp"] else None),
|
||||
initial_stop_loss=data["initial_stop_loss_abs"],
|
||||
initial_stop_loss_pct=data["initial_stop_loss_ratio"],
|
||||
min_rate=data["min_rate"],
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
@@ -635,7 +636,7 @@ def load_and_plot_trades(config: Config):
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||
IStrategy.dp = DataProvider(config, exchange)
|
||||
strategy.ft_bot_start()
|
||||
strategy.bot_loop_start()
|
||||
strategy.bot_loop_start(datetime.now(timezone.utc))
|
||||
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
||||
timerange = plot_elements['timerange']
|
||||
trades = plot_elements['trades']
|
||||
|
@@ -276,6 +276,10 @@ class TradeSchema(BaseModel):
|
||||
funding_fees: Optional[float]
|
||||
trading_mode: Optional[TradingMode]
|
||||
|
||||
amount_precision: Optional[float]
|
||||
price_precision: Optional[float]
|
||||
precision_mode: Optional[int]
|
||||
|
||||
|
||||
class OpenTradeSchema(TradeSchema):
|
||||
stoploss_current_dist: Optional[float]
|
||||
|
@@ -1,9 +1,11 @@
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
from typing import Any, AsyncIterator, Dict, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence.models import _request_id_ctx_var
|
||||
from freqtrade.rpc.rpc import RPC, RPCException
|
||||
|
||||
from .webserver import ApiServer
|
||||
@@ -15,12 +17,19 @@ def get_rpc_optional() -> Optional[RPC]:
|
||||
return None
|
||||
|
||||
|
||||
def get_rpc() -> Optional[Iterator[RPC]]:
|
||||
async def get_rpc() -> Optional[AsyncIterator[RPC]]:
|
||||
|
||||
_rpc = get_rpc_optional()
|
||||
if _rpc:
|
||||
request_id = str(uuid4())
|
||||
ctx_token = _request_id_ctx_var.set(request_id)
|
||||
Trade.rollback()
|
||||
yield _rpc
|
||||
Trade.rollback()
|
||||
try:
|
||||
yield _rpc
|
||||
finally:
|
||||
Trade.session.remove()
|
||||
_request_id_ctx_var.reset(ctx_token)
|
||||
|
||||
else:
|
||||
raise RPCException('Bot is not in the correct state')
|
||||
|
||||
|
@@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
|
||||
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
|
||||
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
||||
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -108,7 +109,7 @@ class ApiServer(RPCHandler):
|
||||
cls._has_rpc = False
|
||||
cls._rpc = None
|
||||
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||
"""
|
||||
Publish the message to the message stream
|
||||
"""
|
||||
|
@@ -5,7 +5,7 @@ import logging
|
||||
from abc import abstractmethod
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from math import isnan
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import arrow
|
||||
import psutil
|
||||
@@ -13,6 +13,7 @@ from dateutil.relativedelta import relativedelta
|
||||
from dateutil.tz import tzlocal
|
||||
from numpy import NAN, inf, int64, mean
|
||||
from pandas import DataFrame, NaT
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
@@ -29,6 +30,7 @@ from freqtrade.persistence import Order, PairLocks, Trade
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||
from freqtrade.wallets import PositionWallet, Wallet
|
||||
|
||||
|
||||
@@ -78,7 +80,7 @@ class RPCHandler:
|
||||
""" Cleanup pending module resources """
|
||||
|
||||
@abstractmethod
|
||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||
""" Sends a message to all registered rpc modules """
|
||||
|
||||
|
||||
@@ -122,7 +124,8 @@ class RPC:
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
|
||||
'stoploss': config.get('stoploss'),
|
||||
'stoploss_on_exchange': config.get('stoploss_on_exchange', False),
|
||||
'stoploss_on_exchange': config.get('order_types',
|
||||
{}).get('stoploss_on_exchange', False),
|
||||
'trailing_stop': config.get('trailing_stop'),
|
||||
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
|
||||
@@ -158,7 +161,7 @@ class RPC:
|
||||
"""
|
||||
# Fetch open trades
|
||||
if trade_ids:
|
||||
trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
|
||||
trades: Sequence[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
|
||||
else:
|
||||
trades = Trade.get_open_trades()
|
||||
|
||||
@@ -339,11 +342,13 @@ class RPC:
|
||||
for day in range(0, timescale):
|
||||
profitday = start_date - time_offset(day)
|
||||
# Only query for necessary columns for performance reasons.
|
||||
trades = Trade.query.session.query(Trade.close_profit_abs).filter(
|
||||
Trade.is_open.is_(False),
|
||||
Trade.close_date >= profitday,
|
||||
Trade.close_date < (profitday + time_offset(1))
|
||||
).order_by(Trade.close_date).all()
|
||||
trades = Trade.session.execute(
|
||||
select(Trade.close_profit_abs)
|
||||
.filter(Trade.is_open.is_(False),
|
||||
Trade.close_date >= profitday,
|
||||
Trade.close_date < (profitday + time_offset(1)))
|
||||
.order_by(Trade.close_date)
|
||||
).all()
|
||||
|
||||
curdayprofit = sum(
|
||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||
@@ -381,14 +386,19 @@ class RPC:
|
||||
""" Returns the X last trades """
|
||||
order_by: Any = Trade.id if order_by_id else Trade.close_date.desc()
|
||||
if limit:
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
|
||||
order_by).limit(limit).offset(offset)
|
||||
trades = Trade.session.scalars(
|
||||
Trade.get_trades_query([Trade.is_open.is_(False)])
|
||||
.order_by(order_by)
|
||||
.limit(limit)
|
||||
.offset(offset))
|
||||
else:
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
|
||||
Trade.close_date.desc())
|
||||
trades = Trade.session.scalars(
|
||||
Trade.get_trades_query([Trade.is_open.is_(False)])
|
||||
.order_by(Trade.close_date.desc()))
|
||||
|
||||
output = [trade.to_json() for trade in trades]
|
||||
total_trades = Trade.get_trades([Trade.is_open.is_(False)]).count()
|
||||
total_trades = Trade.session.scalar(
|
||||
select(func.count(Trade.id)).filter(Trade.is_open.is_(False)))
|
||||
|
||||
return {
|
||||
"trades": output,
|
||||
@@ -436,8 +446,8 @@ class RPC:
|
||||
""" Returns cumulative profit statistics """
|
||||
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
|
||||
Trade.is_open.is_(True))
|
||||
trades: List[Trade] = Trade.get_trades(
|
||||
trade_filter, include_orders=False).order_by(Trade.id).all()
|
||||
trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query(
|
||||
trade_filter, include_orders=False).order_by(Trade.id)).all()
|
||||
|
||||
profit_all_coin = []
|
||||
profit_all_ratio = []
|
||||
@@ -946,12 +956,12 @@ class RPC:
|
||||
def _rpc_delete_lock(self, lockid: Optional[int] = None,
|
||||
pair: Optional[str] = None) -> Dict[str, Any]:
|
||||
""" Delete specific lock(s) """
|
||||
locks = []
|
||||
locks: Sequence[PairLock] = []
|
||||
|
||||
if pair:
|
||||
locks = PairLocks.get_pair_locks(pair)
|
||||
if lockid:
|
||||
locks = PairLock.query.filter(PairLock.id == lockid).all()
|
||||
locks = PairLock.session.scalars(select(PairLock).filter(PairLock.id == lockid)).all()
|
||||
|
||||
for lock in locks:
|
||||
lock.active = False
|
||||
|
@@ -3,11 +3,12 @@ This module contains class to manage RPC communications (Telegram, API, ...)
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Any, Dict, List
|
||||
from typing import List
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType
|
||||
from freqtrade.rpc import RPC, RPCHandler
|
||||
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -58,7 +59,7 @@ class RPCManager:
|
||||
mod.cleanup()
|
||||
del mod
|
||||
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||
"""
|
||||
Send given message to all registered rpc modules.
|
||||
A message consists of one or more key value pairs of strings.
|
||||
@@ -69,10 +70,6 @@ class RPCManager:
|
||||
"""
|
||||
if msg.get('type') not in NO_ECHO_MESSAGES:
|
||||
logger.info('Sending rpc message: %s', msg)
|
||||
if 'pair' in msg:
|
||||
msg.update({
|
||||
'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair'])
|
||||
})
|
||||
for mod in self.registered_modules:
|
||||
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||
try:
|
||||
|
128
freqtrade/rpc/rpc_types.py
Normal file
128
freqtrade/rpc/rpc_types.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Literal, Optional, TypedDict, Union
|
||||
|
||||
from freqtrade.constants import PairWithTimeframe
|
||||
from freqtrade.enums import RPCMessageType
|
||||
|
||||
|
||||
class RPCSendMsgBase(TypedDict):
|
||||
pass
|
||||
# ty1pe: Literal[RPCMessageType]
|
||||
|
||||
|
||||
class RPCStatusMsg(RPCSendMsgBase):
|
||||
"""Used for Status, Startup and Warning messages"""
|
||||
type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING]
|
||||
status: str
|
||||
|
||||
|
||||
class RPCStrategyMsg(RPCSendMsgBase):
|
||||
"""Used for Status, Startup and Warning messages"""
|
||||
type: Literal[RPCMessageType.STRATEGY_MSG]
|
||||
msg: str
|
||||
|
||||
|
||||
class RPCProtectionMsg(RPCSendMsgBase):
|
||||
type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL]
|
||||
id: int
|
||||
pair: str
|
||||
base_currency: Optional[str]
|
||||
lock_time: str
|
||||
lock_timestamp: int
|
||||
lock_end_time: str
|
||||
lock_end_timestamp: int
|
||||
reason: str
|
||||
side: str
|
||||
active: bool
|
||||
|
||||
|
||||
class RPCWhitelistMsg(RPCSendMsgBase):
|
||||
type: Literal[RPCMessageType.WHITELIST]
|
||||
data: List[str]
|
||||
|
||||
|
||||
class __RPCBuyMsgBase(RPCSendMsgBase):
|
||||
trade_id: int
|
||||
buy_tag: Optional[str]
|
||||
enter_tag: Optional[str]
|
||||
exchange: str
|
||||
pair: str
|
||||
base_currency: str
|
||||
leverage: Optional[float]
|
||||
direction: str
|
||||
limit: float
|
||||
open_rate: float
|
||||
order_type: Optional[str] # TODO: why optional??
|
||||
stake_amount: float
|
||||
stake_currency: str
|
||||
fiat_currency: Optional[str]
|
||||
amount: float
|
||||
open_date: datetime
|
||||
current_rate: Optional[float]
|
||||
sub_trade: bool
|
||||
|
||||
|
||||
class RPCBuyMsg(__RPCBuyMsgBase):
|
||||
type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]
|
||||
|
||||
|
||||
class RPCCancelMsg(__RPCBuyMsgBase):
|
||||
type: Literal[RPCMessageType.ENTRY_CANCEL]
|
||||
reason: str
|
||||
|
||||
|
||||
class RPCSellMsg(__RPCBuyMsgBase):
|
||||
type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]
|
||||
cumulative_profit: float
|
||||
gain: str # Literal["profit", "loss"]
|
||||
close_rate: float
|
||||
profit_amount: float
|
||||
profit_ratio: float
|
||||
sell_reason: Optional[str]
|
||||
exit_reason: Optional[str]
|
||||
close_date: datetime
|
||||
# current_rate: Optional[float]
|
||||
order_rate: Optional[float]
|
||||
|
||||
|
||||
class RPCSellCancelMsg(__RPCBuyMsgBase):
|
||||
type: Literal[RPCMessageType.EXIT_CANCEL]
|
||||
reason: str
|
||||
gain: str # Literal["profit", "loss"]
|
||||
profit_amount: float
|
||||
profit_ratio: float
|
||||
sell_reason: Optional[str]
|
||||
exit_reason: Optional[str]
|
||||
close_date: datetime
|
||||
|
||||
|
||||
class _AnalyzedDFData(TypedDict):
|
||||
key: PairWithTimeframe
|
||||
df: Any
|
||||
la: datetime
|
||||
|
||||
|
||||
class RPCAnalyzedDFMsg(RPCSendMsgBase):
|
||||
"""New Analyzed dataframe message"""
|
||||
type: Literal[RPCMessageType.ANALYZED_DF]
|
||||
data: _AnalyzedDFData
|
||||
|
||||
|
||||
class RPCNewCandleMsg(RPCSendMsgBase):
|
||||
"""New candle ping message, issued once per new candle/pair"""
|
||||
type: Literal[RPCMessageType.NEW_CANDLE]
|
||||
data: PairWithTimeframe
|
||||
|
||||
|
||||
RPCSendMsg = Union[
|
||||
RPCStatusMsg,
|
||||
RPCStrategyMsg,
|
||||
RPCProtectionMsg,
|
||||
RPCWhitelistMsg,
|
||||
RPCBuyMsg,
|
||||
RPCCancelMsg,
|
||||
RPCSellMsg,
|
||||
RPCSellCancelMsg,
|
||||
RPCAnalyzedDFMsg,
|
||||
RPCNewCandleMsg
|
||||
]
|
@@ -30,6 +30,7 @@ from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import chunks, plural, round_coin_value
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import RPC, RPCException, RPCHandler
|
||||
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -83,6 +84,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||
self._send_msg(str(e))
|
||||
except BaseException:
|
||||
logger.exception('Exception occurred within Telegram module')
|
||||
finally:
|
||||
Trade.session.remove()
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -427,14 +430,14 @@ class Telegram(RPCHandler):
|
||||
return None
|
||||
return message
|
||||
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||
""" Send a message to telegram channel """
|
||||
|
||||
default_noti = 'on'
|
||||
|
||||
msg_type = msg['type']
|
||||
noti = ''
|
||||
if msg_type == RPCMessageType.EXIT:
|
||||
if msg['type'] == RPCMessageType.EXIT:
|
||||
sell_noti = self._config['telegram'] \
|
||||
.get('notification_settings', {}).get(str(msg_type), {})
|
||||
# For backward compatibility sell still can be string
|
||||
@@ -451,7 +454,7 @@ class Telegram(RPCHandler):
|
||||
# Notification disabled
|
||||
return
|
||||
|
||||
message = self.compose_message(deepcopy(msg), msg_type)
|
||||
message = self.compose_message(deepcopy(msg), msg_type) # type: ignore
|
||||
if message:
|
||||
self._send_msg(message, disable_notification=(noti == 'silent'))
|
||||
|
||||
@@ -1340,7 +1343,7 @@ class Telegram(RPCHandler):
|
||||
message = tabulate({k: [v] for k, v in counts.items()},
|
||||
headers=['current', 'max', 'total stake'],
|
||||
tablefmt='simple')
|
||||
message = "<pre>{}</pre>".format(message)
|
||||
message = f"<pre>{message}</pre>"
|
||||
logger.debug(message)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_count",
|
||||
@@ -1642,7 +1645,7 @@ class Telegram(RPCHandler):
|
||||
])
|
||||
else:
|
||||
reply_markup = InlineKeyboardMarkup([[]])
|
||||
msg += "\nUpdated: {}".format(datetime.now().ctime())
|
||||
msg += f"\nUpdated: {datetime.now().ctime()}"
|
||||
if not query.message:
|
||||
return
|
||||
chat_id = query.message.chat_id
|
||||
|
@@ -10,6 +10,7 @@ from requests import RequestException, post
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import RPCMessageType
|
||||
from freqtrade.rpc import RPC, RPCHandler
|
||||
from freqtrade.rpc.rpc_types import RPCSendMsg
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -41,7 +42,7 @@ class Webhook(RPCHandler):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]:
|
||||
whconfig = self._config['webhook']
|
||||
# Deprecated 2022.10 - only keep generic method.
|
||||
if msg['type'] in [RPCMessageType.ENTRY]:
|
||||
@@ -75,7 +76,7 @@ class Webhook(RPCHandler):
|
||||
return None
|
||||
return valuedict
|
||||
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
def send_msg(self, msg: RPCSendMsg) -> None:
|
||||
""" Send a message to telegram channel """
|
||||
try:
|
||||
|
||||
@@ -113,7 +114,7 @@ class Webhook(RPCHandler):
|
||||
response = post(self._url, data=payload['data'],
|
||||
headers={'Content-Type': 'text/plain'})
|
||||
else:
|
||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||
raise NotImplementedError(f'Unknown format: {self._format}')
|
||||
|
||||
# Throw a RequestException if the post was not successful
|
||||
response.raise_for_status()
|
||||
|
@@ -251,11 +251,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
pass
|
||||
|
||||
def bot_loop_start(self, **kwargs) -> None:
|
||||
def bot_loop_start(self, current_time: datetime, **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 current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
pass
|
||||
|
255
freqtrade/strategy/strategyupdater.py
Normal file
255
freqtrade/strategy/strategyupdater.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import ast_comments
|
||||
|
||||
from freqtrade.constants import Config
|
||||
|
||||
|
||||
class StrategyUpdater:
|
||||
name_mapping = {
|
||||
'ticker_interval': 'timeframe',
|
||||
'buy': 'enter_long',
|
||||
'sell': 'exit_long',
|
||||
'buy_tag': 'enter_tag',
|
||||
'sell_reason': 'exit_reason',
|
||||
|
||||
'sell_signal': 'exit_signal',
|
||||
'custom_sell': 'custom_exit',
|
||||
'force_sell': 'force_exit',
|
||||
'emergency_sell': 'emergency_exit',
|
||||
|
||||
# Strategy/config settings:
|
||||
'use_sell_signal': 'use_exit_signal',
|
||||
'sell_profit_only': 'exit_profit_only',
|
||||
'sell_profit_offset': 'exit_profit_offset',
|
||||
'ignore_roi_if_buy_signal': 'ignore_roi_if_entry_signal',
|
||||
'forcebuy_enable': 'force_entry_enable',
|
||||
}
|
||||
|
||||
function_mapping = {
|
||||
'populate_buy_trend': 'populate_entry_trend',
|
||||
'populate_sell_trend': 'populate_exit_trend',
|
||||
'custom_sell': 'custom_exit',
|
||||
'check_buy_timeout': 'check_entry_timeout',
|
||||
'check_sell_timeout': 'check_exit_timeout',
|
||||
# '': '',
|
||||
}
|
||||
# order_time_in_force, order_types, unfilledtimeout
|
||||
otif_ot_unfilledtimeout = {
|
||||
'buy': 'entry',
|
||||
'sell': 'exit',
|
||||
}
|
||||
|
||||
# create a dictionary that maps the old column names to the new ones
|
||||
rename_dict = {'buy': 'enter_long', 'sell': 'exit_long', 'buy_tag': 'enter_tag'}
|
||||
|
||||
def start(self, config: Config, strategy_obj: dict) -> None:
|
||||
"""
|
||||
Run strategy updater
|
||||
It updates a strategy to v3 with the help of the ast-module
|
||||
:return: None
|
||||
"""
|
||||
|
||||
source_file = strategy_obj['location']
|
||||
strategies_backup_folder = Path.joinpath(config['user_data_dir'], "strategies_orig_updater")
|
||||
target_file = Path.joinpath(strategies_backup_folder, strategy_obj['location_rel'])
|
||||
|
||||
# read the file
|
||||
with Path(source_file).open('r') as f:
|
||||
old_code = f.read()
|
||||
if not strategies_backup_folder.is_dir():
|
||||
Path(strategies_backup_folder).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# backup original
|
||||
# => currently no date after the filename,
|
||||
# could get overridden pretty fast if this is fired twice!
|
||||
# The folder is always the same and the file name too (currently).
|
||||
shutil.copy(source_file, target_file)
|
||||
|
||||
# update the code
|
||||
new_code = self.update_code(old_code)
|
||||
# write the modified code to the destination folder
|
||||
with Path(source_file).open('w') as f:
|
||||
f.write(new_code)
|
||||
|
||||
# define the function to update the code
|
||||
def update_code(self, code):
|
||||
# parse the code into an AST
|
||||
tree = ast_comments.parse(code)
|
||||
|
||||
# use the AST to update the code
|
||||
updated_code = self.modify_ast(tree)
|
||||
|
||||
# return the modified code without executing it
|
||||
return updated_code
|
||||
|
||||
# function that uses the ast module to update the code
|
||||
def modify_ast(self, tree): # noqa
|
||||
# use the visitor to update the names and functions in the AST
|
||||
NameUpdater().visit(tree)
|
||||
|
||||
# first fix the comments, so it understands "\n" properly inside multi line comments.
|
||||
ast_comments.fix_missing_locations(tree)
|
||||
ast_comments.increment_lineno(tree, n=1)
|
||||
|
||||
# generate the new code from the updated AST
|
||||
# without indent {} parameters would just be written straight one after the other.
|
||||
|
||||
# ast_comments would be amazing since this is the only solution that carries over comments,
|
||||
# but it does currently not have an unparse function, hopefully in the future ... !
|
||||
# return ast_comments.unparse(tree)
|
||||
|
||||
return ast_comments.unparse(tree)
|
||||
|
||||
|
||||
# Here we go through each respective node, slice, elt, key ... to replace outdated entries.
|
||||
class NameUpdater(ast_comments.NodeTransformer):
|
||||
def generic_visit(self, node):
|
||||
|
||||
# space is not yet transferred from buy/sell to entry/exit and thereby has to be skipped.
|
||||
if isinstance(node, ast_comments.keyword):
|
||||
if node.arg == "space":
|
||||
return node
|
||||
|
||||
# from here on this is the original function.
|
||||
for field, old_value in ast_comments.iter_fields(node):
|
||||
if isinstance(old_value, list):
|
||||
new_values = []
|
||||
for value in old_value:
|
||||
if isinstance(value, ast_comments.AST):
|
||||
value = self.visit(value)
|
||||
if value is None:
|
||||
continue
|
||||
elif not isinstance(value, ast_comments.AST):
|
||||
new_values.extend(value)
|
||||
continue
|
||||
new_values.append(value)
|
||||
old_value[:] = new_values
|
||||
elif isinstance(old_value, ast_comments.AST):
|
||||
new_node = self.visit(old_value)
|
||||
if new_node is None:
|
||||
delattr(node, field)
|
||||
else:
|
||||
setattr(node, field, new_node)
|
||||
return node
|
||||
|
||||
def visit_Expr(self, node):
|
||||
if hasattr(node.value, "left") and hasattr(node.value.left, "id"):
|
||||
node.value.left.id = self.check_dict(StrategyUpdater.name_mapping, node.value.left.id)
|
||||
self.visit(node.value)
|
||||
return node
|
||||
|
||||
# Renames an element if contained inside a dictionary.
|
||||
@staticmethod
|
||||
def check_dict(current_dict: dict, element: str):
|
||||
if element in current_dict:
|
||||
element = current_dict[element]
|
||||
return element
|
||||
|
||||
def visit_arguments(self, node):
|
||||
if isinstance(node.args, list):
|
||||
for arg in node.args:
|
||||
arg.arg = self.check_dict(StrategyUpdater.name_mapping, arg.arg)
|
||||
return node
|
||||
|
||||
def visit_Name(self, node):
|
||||
# if the name is in the mapping, update it
|
||||
node.id = self.check_dict(StrategyUpdater.name_mapping, node.id)
|
||||
return node
|
||||
|
||||
def visit_Import(self, node):
|
||||
# do not update the names in import statements
|
||||
return node
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
# if hasattr(node, "module"):
|
||||
# if node.module == "freqtrade.strategy.hyper":
|
||||
# node.module = "freqtrade.strategy"
|
||||
return node
|
||||
|
||||
def visit_If(self, node: ast_comments.If):
|
||||
for child in ast_comments.iter_child_nodes(node):
|
||||
self.visit(child)
|
||||
return node
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
node.name = self.check_dict(StrategyUpdater.function_mapping, node.name)
|
||||
self.generic_visit(node)
|
||||
return node
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
if (
|
||||
isinstance(node.value, ast_comments.Name)
|
||||
and node.value.id == 'trade'
|
||||
and node.attr == 'nr_of_successful_buys'
|
||||
):
|
||||
node.attr = 'nr_of_successful_entries'
|
||||
return node
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
# check if the class is derived from IStrategy
|
||||
if any(isinstance(base, ast_comments.Name) and
|
||||
base.id == 'IStrategy' for base in node.bases):
|
||||
# check if the INTERFACE_VERSION variable exists
|
||||
has_interface_version = any(
|
||||
isinstance(child, ast_comments.Assign) and
|
||||
isinstance(child.targets[0], ast_comments.Name) and
|
||||
child.targets[0].id == 'INTERFACE_VERSION'
|
||||
for child in node.body
|
||||
)
|
||||
|
||||
# if the INTERFACE_VERSION variable does not exist, add it as the first child
|
||||
if not has_interface_version:
|
||||
node.body.insert(0, ast_comments.parse('INTERFACE_VERSION = 3').body[0])
|
||||
# otherwise, update its value to 3
|
||||
else:
|
||||
for child in node.body:
|
||||
if (
|
||||
isinstance(child, ast_comments.Assign)
|
||||
and isinstance(child.targets[0], ast_comments.Name)
|
||||
and child.targets[0].id == 'INTERFACE_VERSION'
|
||||
):
|
||||
child.value = ast_comments.parse('3').body[0].value
|
||||
self.generic_visit(node)
|
||||
return node
|
||||
|
||||
def visit_Subscript(self, node):
|
||||
if isinstance(node.slice, ast_comments.Constant):
|
||||
if node.slice.value in StrategyUpdater.rename_dict:
|
||||
# Replace the slice attributes with the values from rename_dict
|
||||
node.slice.value = StrategyUpdater.rename_dict[node.slice.value]
|
||||
if hasattr(node.slice, "elts"):
|
||||
self.visit_elts(node.slice.elts)
|
||||
if hasattr(node.slice, "value"):
|
||||
if hasattr(node.slice.value, "elts"):
|
||||
self.visit_elts(node.slice.value.elts)
|
||||
return node
|
||||
|
||||
# elts can have elts (technically recursively)
|
||||
def visit_elts(self, elts):
|
||||
if isinstance(elts, list):
|
||||
for elt in elts:
|
||||
self.visit_elt(elt)
|
||||
else:
|
||||
self.visit_elt(elts)
|
||||
return elts
|
||||
|
||||
# sub function again needed since the structure itself is highly flexible ...
|
||||
def visit_elt(self, elt):
|
||||
if isinstance(elt, ast_comments.Constant) and elt.value in StrategyUpdater.rename_dict:
|
||||
elt.value = StrategyUpdater.rename_dict[elt.value]
|
||||
if hasattr(elt, "elts"):
|
||||
self.visit_elts(elt.elts)
|
||||
if hasattr(elt, "args"):
|
||||
if isinstance(elt.args, ast_comments.arguments):
|
||||
self.visit_elts(elt.args)
|
||||
else:
|
||||
for arg in elt.args:
|
||||
self.visit_elts(arg)
|
||||
return elt
|
||||
|
||||
def visit_Constant(self, node):
|
||||
node.value = self.check_dict(StrategyUpdater.otif_ot_unfilledtimeout, node.value)
|
||||
node.value = self.check_dict(StrategyUpdater.name_mapping, node.value)
|
||||
return node
|
@@ -1,5 +1,5 @@
|
||||
|
||||
def bot_loop_start(self, **kwargs) -> None:
|
||||
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||||
"""
|
||||
Called at the start of the bot iteration (one loop).
|
||||
Might be used to perform pair-independent tasks
|
||||
@@ -8,6 +8,7 @@ def bot_loop_start(self, **kwargs) -> None:
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, this simply does nothing.
|
||||
: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.
|
||||
"""
|
||||
pass
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
from packaging import version
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums.tradingmode import TradingMode
|
||||
@@ -44,7 +45,7 @@ def _migrate_binance_futures_db(config: Config):
|
||||
# Should symbol be migrated too?
|
||||
# order.symbol = new_pair
|
||||
Trade.commit()
|
||||
pls = PairLock.query.filter(PairLock.pair.notlike('%:%'))
|
||||
pls = PairLock.session.scalars(select(PairLock).filter(PairLock.pair.notlike('%:%'))).all()
|
||||
for pl in pls:
|
||||
pl.pair = f"{pl.pair}:{config['stake_currency']}"
|
||||
# print(pls)
|
||||
|
8
freqtrade/vendor/qtpylib/indicators.py
vendored
8
freqtrade/vendor/qtpylib/indicators.py
vendored
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# QTPyLib: Quantitative Trading Python Library
|
||||
# https://github.com/ranaroussi/qtpylib
|
||||
#
|
||||
@@ -18,7 +16,6 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -27,11 +24,6 @@ import pandas as pd
|
||||
from pandas.core.base import PandasObject
|
||||
|
||||
|
||||
# =============================================
|
||||
# check min, python version
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemError("QTPyLib requires Python version >= 3.4")
|
||||
|
||||
# =============================================
|
||||
warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
||||
|
||||
|
Reference in New Issue
Block a user