Merge remote-tracking branch 'origin/develop' into feat/add-pytorch-model-support
This commit is contained in:
		| @@ -1,5 +1,5 @@ | ||||
| """ Freqtrade bot """ | ||||
| __version__ = '2023.3.dev' | ||||
| __version__ = '2023.4.dev' | ||||
|  | ||||
| if 'dev' in __version__: | ||||
|     from pathlib import Path | ||||
|   | ||||
| @@ -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' | ||||
| @@ -588,6 +589,7 @@ CONF_SCHEMA = { | ||||
|                 "rl_config": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "drop_ohlc_from_features": {"type": "boolean", "default": False}, | ||||
|                         "train_cycles": {"type": "integer"}, | ||||
|                         "max_trade_duration_candles": {"type": "integer"}, | ||||
|                         "add_state_info": {"type": "boolean", "default": False}, | ||||
| @@ -596,7 +598,7 @@ CONF_SCHEMA = { | ||||
|                         "model_type": {"type": "string", "default": "PPO"}, | ||||
|                         "policy_type": {"type": "string", "default": "MlpPolicy"}, | ||||
|                         "net_arch": {"type": "array", "default": [128, 128]}, | ||||
|                         "randomize_startinng_position": {"type": "boolean", "default": False}, | ||||
|                         "randomize_starting_position": {"type": "boolean", "default": False}, | ||||
|                         "model_reward_parameters": { | ||||
|                             "type": "object", | ||||
|                             "properties": { | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from enum import Enum | ||||
| class RPCMessageType(str, Enum): | ||||
|     STATUS = 'status' | ||||
|     WARNING = 'warning' | ||||
|     EXCEPTION = 'exception' | ||||
|     STARTUP = 'startup' | ||||
|  | ||||
|     ENTRY = 'entry' | ||||
|   | ||||
| @@ -8,15 +8,15 @@ from freqtrade.exchange.bitpanda import Bitpanda | ||||
| from freqtrade.exchange.bittrex import Bittrex | ||||
| from freqtrade.exchange.bybit import Bybit | ||||
| from freqtrade.exchange.coinbasepro import Coinbasepro | ||||
| from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts, | ||||
|                                                amount_to_precision, available_exchanges, | ||||
|                                                ccxt_exchanges, contracts_to_amount, | ||||
|                                                date_minus_candles, is_exchange_known_ccxt, | ||||
|                                                market_is_active, price_to_precision, | ||||
|                                                timeframe_to_minutes, timeframe_to_msecs, | ||||
|                                                timeframe_to_next_date, timeframe_to_prev_date, | ||||
|                                                timeframe_to_seconds, validate_exchange, | ||||
|                                                validate_exchanges) | ||||
| from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, | ||||
|                                                amount_to_contracts, amount_to_precision, | ||||
|                                                available_exchanges, ccxt_exchanges, | ||||
|                                                contracts_to_amount, date_minus_candles, | ||||
|                                                is_exchange_known_ccxt, market_is_active, | ||||
|                                                price_to_precision, timeframe_to_minutes, | ||||
|                                                timeframe_to_msecs, timeframe_to_next_date, | ||||
|                                                timeframe_to_prev_date, timeframe_to_seconds, | ||||
|                                                validate_exchange, validate_exchanges) | ||||
| from freqtrade.exchange.gate import Gate | ||||
| from freqtrade.exchange.hitbtc import Hitbtc | ||||
| from freqtrade.exchange.huobi import Huobi | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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) | ||||
|   | ||||
| @@ -30,13 +30,14 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun | ||||
|                                   RetryableOrderError, TemporaryError) | ||||
| from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier, | ||||
|                                        retrier_async) | ||||
| from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision, | ||||
|                                                amount_to_contracts, amount_to_precision, | ||||
|                                                contracts_to_amount, date_minus_candles, | ||||
|                                                is_exchange_known_ccxt, market_is_active, | ||||
|                                                price_to_precision, timeframe_to_minutes, | ||||
|                                                timeframe_to_msecs, timeframe_to_next_date, | ||||
|                                                timeframe_to_prev_date, timeframe_to_seconds) | ||||
| from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType, | ||||
|                                                amount_to_contract_precision, amount_to_contracts, | ||||
|                                                amount_to_precision, contracts_to_amount, | ||||
|                                                date_minus_candles, is_exchange_known_ccxt, | ||||
|                                                market_is_active, price_to_precision, | ||||
|                                                timeframe_to_minutes, timeframe_to_msecs, | ||||
|                                                timeframe_to_next_date, timeframe_to_prev_date, | ||||
|                                                timeframe_to_seconds) | ||||
| from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers | ||||
| from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json, | ||||
|                             safe_value_fallback2) | ||||
| @@ -59,8 +60,8 @@ class Exchange: | ||||
|     # or by specifying them in the configuration. | ||||
|     _ft_has_default: Dict = { | ||||
|         "stoploss_on_exchange": False, | ||||
|         "stop_price_param": "stopPrice", | ||||
|         "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 | ||||
| @@ -69,6 +70,7 @@ class Exchange: | ||||
|         # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency | ||||
|         "ohlcv_volume_currency": "base",  # "base" or "quote" | ||||
|         "tickers_have_quoteVolume": True, | ||||
|         "tickers_have_bid_ask": True,  # bid / ask empty for fetch_tickers | ||||
|         "tickers_have_price": True, | ||||
|         "trades_pagination": "time",  # Possible are "time" or "id" | ||||
|         "trades_pagination_arg": "since", | ||||
| @@ -80,6 +82,8 @@ class Exchange: | ||||
|         "fee_cost_in_contracts": False,  # Fee cost needs contract conversion | ||||
|         "needs_trading_fees": False,  # use fetch_trading_fees to cache fees | ||||
|         "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], | ||||
|         # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong | ||||
|         "marketOrderRequiresPrice": False, | ||||
|     } | ||||
|     _ft_has: Dict = {} | ||||
|     _ft_has_futures: Dict = {} | ||||
| @@ -205,6 +209,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 | ||||
| @@ -730,12 +736,14 @@ class Exchange: | ||||
|         """ | ||||
|         return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode) | ||||
|  | ||||
|     def price_to_precision(self, pair: str, price: float) -> float: | ||||
|     def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float: | ||||
|         """ | ||||
|         Returns the price rounded up to the precision the Exchange accepts. | ||||
|         Rounds up | ||||
|         Returns the price rounded to the precision the Exchange accepts. | ||||
|         The default price_rounding_mode in conf is ROUND. | ||||
|         For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts. | ||||
|         """ | ||||
|         return price_to_precision(price, self.get_precision_price(pair), self.precisionMode) | ||||
|         return price_to_precision(price, self.get_precision_price(pair), | ||||
|                                   self.precisionMode, rounding_mode=rounding_mode) | ||||
|  | ||||
|     def price_get_one_pip(self, pair: str, price: float) -> float: | ||||
|         """ | ||||
| @@ -758,12 +766,12 @@ class Exchange: | ||||
|         return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage) | ||||
|  | ||||
|     def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float: | ||||
|         max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max') | ||||
|         max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage) | ||||
|         if max_stake_amount is None: | ||||
|             # * Should never be executed | ||||
|             raise OperationalException(f'{self.name}.get_max_pair_stake_amount should' | ||||
|                                        'never set max_stake_amount to None') | ||||
|         return max_stake_amount / leverage | ||||
|         return max_stake_amount | ||||
|  | ||||
|     def _get_stake_amount_limit( | ||||
|         self, | ||||
| @@ -781,43 +789,41 @@ class Exchange: | ||||
|         except KeyError: | ||||
|             raise ValueError(f"Can't get market information for symbol {pair}") | ||||
|  | ||||
|         if isMin: | ||||
|             # reserve some percent defined in config (5% default) + stoploss | ||||
|             margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent', | ||||
|                                                            DEFAULT_AMOUNT_RESERVE_PERCENT) | ||||
|             stoploss_reserve = ( | ||||
|                 margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 | ||||
|             ) | ||||
|             # it should not be more than 50% | ||||
|             stoploss_reserve = max(min(stoploss_reserve, 1.5), 1) | ||||
|         else: | ||||
|             margin_reserve = 1.0 | ||||
|             stoploss_reserve = 1.0 | ||||
|  | ||||
|         stake_limits = [] | ||||
|         limits = market['limits'] | ||||
|         if (limits['cost'][limit] is not None): | ||||
|             stake_limits.append( | ||||
|                 self._contracts_to_amount( | ||||
|                     pair, | ||||
|                     limits['cost'][limit] | ||||
|                 ) | ||||
|                 self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve | ||||
|             ) | ||||
|  | ||||
|         if (limits['amount'][limit] is not None): | ||||
|             stake_limits.append( | ||||
|                 self._contracts_to_amount( | ||||
|                     pair, | ||||
|                     limits['amount'][limit] * price | ||||
|                 ) | ||||
|                 self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve | ||||
|             ) | ||||
|  | ||||
|         if not stake_limits: | ||||
|             return None if isMin else float('inf') | ||||
|  | ||||
|         # reserve some percent defined in config (5% default) + stoploss | ||||
|         amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', | ||||
|                                                         DEFAULT_AMOUNT_RESERVE_PERCENT) | ||||
|         amount_reserve_percent = ( | ||||
|             amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 | ||||
|         ) | ||||
|         # it should not be more than 50% | ||||
|         amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) | ||||
|  | ||||
|         # The value returned should satisfy both limits: for amount (base currency) and | ||||
|         # for cost (quote, stake currency), so max() is used here. | ||||
|         # See also #2575 at github. | ||||
|         return self._get_stake_amount_considering_leverage( | ||||
|             max(stake_limits) * amount_reserve_percent, | ||||
|             max(stake_limits) if isMin else min(stake_limits), | ||||
|             leverage or 1.0 | ||||
|         ) if isMin else min(stake_limits) | ||||
|         ) | ||||
|  | ||||
|     def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float: | ||||
|         """ | ||||
| @@ -1018,10 +1024,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, | ||||
| @@ -1033,12 +1039,18 @@ 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 | ||||
|  | ||||
|     def _order_needs_price(self, ordertype: str) -> bool: | ||||
|         return ( | ||||
|             ordertype != 'market' | ||||
|             or self._api.options.get("createMarketBuyOrderRequiresPrice", False) | ||||
|             or self._ft_has.get('marketOrderRequiresPrice', False) | ||||
|         ) | ||||
|  | ||||
|     def create_order( | ||||
|         self, | ||||
|         *, | ||||
| @@ -1061,8 +1073,7 @@ class Exchange: | ||||
|         try: | ||||
|             # Set the precision for amount and price(rate) as accepted by the exchange | ||||
|             amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) | ||||
|             needs_price = (ordertype != 'market' | ||||
|                            or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) | ||||
|             needs_price = self._order_needs_price(ordertype) | ||||
|             rate_for_order = self.price_to_precision(pair, rate) if needs_price else None | ||||
|  | ||||
|             if not reduceOnly: | ||||
| @@ -1086,7 +1097,7 @@ class Exchange: | ||||
|                 f'Tried to {side} amount {amount} at rate {rate}.' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.InvalidOrder as e: | ||||
|             raise ExchangeError( | ||||
|             raise InvalidOrderException( | ||||
|                 f'Could not create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to {side} amount {amount} at rate {rate}. ' | ||||
|                 f'Message: {e}') from e | ||||
| @@ -1105,11 +1116,11 @@ class Exchange: | ||||
|         """ | ||||
|         if not self._ft_has.get('stoploss_on_exchange'): | ||||
|             raise OperationalException(f"stoploss is not implemented for {self.name}.") | ||||
|  | ||||
|         price_param = self._ft_has['stop_price_param'] | ||||
|         return ( | ||||
|             order.get('stopPrice', None) is None | ||||
|             or ((side == "sell" and stop_loss > float(order['stopPrice'])) or | ||||
|                 (side == "buy" and stop_loss < float(order['stopPrice']))) | ||||
|             order.get(price_param, None) is None | ||||
|             or ((side == "sell" and stop_loss > float(order[price_param])) or | ||||
|                 (side == "buy" and stop_loss < float(order[price_param]))) | ||||
|         ) | ||||
|  | ||||
|     def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]: | ||||
| @@ -1136,14 +1147,21 @@ class Exchange: | ||||
|                           "sell" else (stop_price >= limit_rate)) | ||||
|         # Ensure rate is less than stop price | ||||
|         if bad_stop_price: | ||||
|             raise OperationalException( | ||||
|                 'In stoploss limit order, stop price should be more than limit price') | ||||
|             # 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}" | ||||
|                 ) | ||||
|         return limit_rate | ||||
|  | ||||
|     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({'stopPrice': stop_price}) | ||||
|         # Verify if stopPrice works for your exchange, else configure stop_price_param | ||||
|         params.update({self._ft_has['stop_price_param']: stop_price}) | ||||
|         return params | ||||
|  | ||||
|     @retrier(retries=0) | ||||
| @@ -1169,12 +1187,12 @@ class Exchange: | ||||
|  | ||||
|         user_order_type = order_types.get('stoploss', 'market') | ||||
|         ordertype, user_order_type = self._get_stop_order_type(user_order_type) | ||||
|  | ||||
|         stop_price_norm = self.price_to_precision(pair, stop_price) | ||||
|         round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP | ||||
|         stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode) | ||||
|         limit_rate = None | ||||
|         if user_order_type == 'limit': | ||||
|             limit_rate = self._get_stop_limit_rate(stop_price, order_types, side) | ||||
|             limit_rate = self.price_to_precision(pair, limit_rate) | ||||
|             limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode) | ||||
|  | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order( | ||||
| @@ -1200,7 +1218,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) | ||||
| @@ -2525,7 +2543,6 @@ class Exchange: | ||||
|         self, | ||||
|         leverage: float, | ||||
|         pair: Optional[str] = None, | ||||
|         trading_mode: Optional[TradingMode] = None, | ||||
|         accept_fail: bool = False, | ||||
|     ): | ||||
|         """ | ||||
| @@ -2543,7 +2560,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 | ||||
| @@ -2754,10 +2771,10 @@ class Exchange: | ||||
|             raise OperationalException( | ||||
|                 f"{self.name} does not support {self.margin_mode} {self.trading_mode}") | ||||
|  | ||||
|         isolated_liq = None | ||||
|         liquidation_price = None | ||||
|         if self._config['dry_run'] or not self.exchange_has("fetchPositions"): | ||||
|  | ||||
|             isolated_liq = self.dry_run_liquidation_price( | ||||
|             liquidation_price = self.dry_run_liquidation_price( | ||||
|                 pair=pair, | ||||
|                 open_rate=open_rate, | ||||
|                 is_short=is_short, | ||||
| @@ -2772,16 +2789,16 @@ class Exchange: | ||||
|             positions = self.fetch_positions(pair) | ||||
|             if len(positions) > 0: | ||||
|                 pos = positions[0] | ||||
|                 isolated_liq = pos['liquidationPrice'] | ||||
|                 liquidation_price = pos['liquidationPrice'] | ||||
|  | ||||
|         if isolated_liq is not None: | ||||
|             buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer | ||||
|             isolated_liq = ( | ||||
|                 isolated_liq - buffer_amount | ||||
|         if liquidation_price is not None: | ||||
|             buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer | ||||
|             liquidation_price_buffer = ( | ||||
|                 liquidation_price - buffer_amount | ||||
|                 if is_short else | ||||
|                 isolated_liq + buffer_amount | ||||
|                 liquidation_price + buffer_amount | ||||
|             ) | ||||
|             return isolated_liq | ||||
|             return max(liquidation_price_buffer, 0.0) | ||||
|         else: | ||||
|             return None | ||||
|  | ||||
|   | ||||
| @@ -2,11 +2,12 @@ | ||||
| Exchange support utils | ||||
| """ | ||||
| from datetime import datetime, timedelta, timezone | ||||
| from math import ceil | ||||
| from math import ceil, floor | ||||
| from typing import Any, Dict, List, Optional, Tuple | ||||
|  | ||||
| import ccxt | ||||
| from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision | ||||
| from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE, | ||||
|                   TRUNCATE, decimal_to_precision) | ||||
|  | ||||
| from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED | ||||
| from freqtrade.util import FtPrecise | ||||
| @@ -219,35 +220,51 @@ def amount_to_contract_precision( | ||||
|     return amount | ||||
|  | ||||
|  | ||||
| def price_to_precision(price: float, price_precision: Optional[float], | ||||
|                        precisionMode: Optional[int]) -> float: | ||||
| def price_to_precision( | ||||
|     price: float, | ||||
|     price_precision: Optional[float], | ||||
|     precisionMode: Optional[int], | ||||
|     *, | ||||
|     rounding_mode: int = ROUND, | ||||
| ) -> float: | ||||
|     """ | ||||
|     Returns the price rounded up to the precision the Exchange accepts. | ||||
|     Returns the price rounded to the precision the Exchange accepts. | ||||
|     Partial Re-implementation of ccxt internal method decimal_to_precision(), | ||||
|     which does not support rounding up | ||||
|     which does not support rounding up. | ||||
|     For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts. | ||||
|  | ||||
|     TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and | ||||
|     align with amount_to_precision(). | ||||
|     !!! Rounds up | ||||
|     :param price: price to convert | ||||
|     :param price_precision: price precision to use. Used from markets[pair]['precision']['price'] | ||||
|     :param precisionMode: precision mode to use. Should be used from precisionMode | ||||
|                           one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE | ||||
|     :param rounding_mode: rounding mode to use. Defaults to ROUND | ||||
|     :return: price rounded up to the precision the Exchange accepts | ||||
|  | ||||
|     """ | ||||
|     if price_precision is not None and precisionMode is not None: | ||||
|         # price = float(decimal_to_precision(price, rounding_mode=ROUND, | ||||
|         #                                    precision=price_precision, | ||||
|         #                                    counting_mode=self.precisionMode, | ||||
|         #                                    )) | ||||
|         if precisionMode == TICK_SIZE: | ||||
|             if rounding_mode == ROUND: | ||||
|                 ticks = price / price_precision | ||||
|                 rounded_ticks = round(ticks) | ||||
|                 return rounded_ticks * price_precision | ||||
|             precision = FtPrecise(price_precision) | ||||
|             price_str = FtPrecise(price) | ||||
|             missing = price_str % precision | ||||
|             if not missing == FtPrecise("0"): | ||||
|                 price = round(float(str(price_str - missing + precision)), 14) | ||||
|         else: | ||||
|             symbol_prec = price_precision | ||||
|             big_price = price * pow(10, symbol_prec) | ||||
|             price = ceil(big_price) / pow(10, symbol_prec) | ||||
|                 return round(float(str(price_str - missing + precision)), 14) | ||||
|             return price | ||||
|         elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES): | ||||
|             ndigits = round(price_precision) | ||||
|             if rounding_mode == ROUND: | ||||
|                 return round(price, ndigits) | ||||
|             ticks = price * (10**ndigits) | ||||
|             if rounding_mode == ROUND_UP: | ||||
|                 return ceil(ticks) / (10**ndigits) | ||||
|             if rounding_mode == TRUNCATE: | ||||
|                 return int(ticks) / (10**ndigits) | ||||
|             if rounding_mode == ROUND_DOWN: | ||||
|                 return floor(ticks) / (10**ndigits) | ||||
|             raise ValueError(f"Unknown rounding_mode {rounding_mode}") | ||||
|         raise ValueError(f"Unknown precisionMode {precisionMode}") | ||||
|     return price | ||||
|   | ||||
| @@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Tuple | ||||
|  | ||||
| from freqtrade.constants import BuySell | ||||
| from freqtrade.enums import MarginMode, PriceType, TradingMode | ||||
| from freqtrade.exceptions import OperationalException | ||||
| from freqtrade.exchange import Exchange | ||||
| from freqtrade.misc import safe_value_fallback2 | ||||
|  | ||||
| @@ -28,10 +27,13 @@ class Gate(Exchange): | ||||
|         "order_time_in_force": ['GTC', 'IOC'], | ||||
|         "stoploss_order_types": {"limit": "limit"}, | ||||
|         "stoploss_on_exchange": True, | ||||
|         "marketOrderRequiresPrice": True, | ||||
|     } | ||||
|  | ||||
|     _ft_has_futures: Dict = { | ||||
|         "needs_trading_fees": True, | ||||
|         "marketOrderRequiresPrice": False, | ||||
|         "tickers_have_bid_ask": False, | ||||
|         "fee_cost_in_contracts": False,  # Set explicitly to false for clarity | ||||
|         "order_props_in_contracts": ['amount', 'filled', 'remaining'], | ||||
|         "stop_price_type_field": "price_type", | ||||
| @@ -49,14 +51,6 @@ class Gate(Exchange): | ||||
|         (TradingMode.FUTURES, MarginMode.ISOLATED) | ||||
|     ] | ||||
|  | ||||
|     def validate_ordertypes(self, order_types: Dict) -> None: | ||||
|  | ||||
|         if self.trading_mode != TradingMode.FUTURES: | ||||
|             if any(v == 'market' for k, v in order_types.items()): | ||||
|                 raise OperationalException( | ||||
|                     f'Exchange {self.name} does not support market orders.') | ||||
|         super().validate_stop_ordertypes(order_types) | ||||
|  | ||||
|     def _get_params( | ||||
|             self, | ||||
|             side: BuySell, | ||||
| @@ -74,8 +68,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, | ||||
|   | ||||
| @@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali | ||||
|                                   OperationalException, TemporaryError) | ||||
| from freqtrade.exchange import Exchange | ||||
| from freqtrade.exchange.common import retrier | ||||
| from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP | ||||
| from freqtrade.exchange.types import Tickers | ||||
|  | ||||
|  | ||||
| @@ -109,6 +110,7 @@ class Kraken(Exchange): | ||||
|         if self.trading_mode == TradingMode.FUTURES: | ||||
|             params.update({'reduceOnly': True}) | ||||
|  | ||||
|         round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP | ||||
|         if order_types.get('stoploss', 'market') == 'limit': | ||||
|             ordertype = "stop-loss-limit" | ||||
|             limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) | ||||
| @@ -116,11 +118,11 @@ class Kraken(Exchange): | ||||
|                 limit_rate = stop_price * limit_price_pct | ||||
|             else: | ||||
|                 limit_rate = stop_price * (2 - limit_price_pct) | ||||
|             params['price2'] = self.price_to_precision(pair, limit_rate) | ||||
|             params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode) | ||||
|         else: | ||||
|             ordertype = "stop-loss" | ||||
|  | ||||
|         stop_price = self.price_to_precision(pair, stop_price) | ||||
|         stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode) | ||||
|  | ||||
|         if self._config['dry_run']: | ||||
|             dry_order = self.create_dry_run_order( | ||||
| @@ -158,7 +160,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,14 @@ 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, | ||||
|         "stop_price_param": "stopLossPrice", | ||||
|     } | ||||
|     _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 +126,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 +161,61 @@ 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 = super()._get_stop_params(side, ordertype, 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 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): | ||||
| @@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment): | ||||
|             elif action == Actions.Sell.value and not self.can_short: | ||||
|                 self._update_total_profit() | ||||
|                 self._position = Positions.Neutral | ||||
|                 trade_type = "neutral" | ||||
|                 trade_type = "exit" | ||||
|                 self._last_trade_tick = None | ||||
|             else: | ||||
|                 print("case not defined") | ||||
| @@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment): | ||||
|             if trade_type is not None: | ||||
|                 self.trade_history.append( | ||||
|                     {'price': self.current_price(), 'index': self._current_tick, | ||||
|                      'type': trade_type}) | ||||
|                      'type': trade_type, 'profit': self.get_unrealized_profit()}) | ||||
|  | ||||
|         if (self._total_profit < self.max_drawdown or | ||||
|                 self._total_unrealized_profit < self.max_drawdown): | ||||
|   | ||||
| @@ -48,20 +48,10 @@ 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): | ||||
|             """ | ||||
|             Action: Neutral, position: Long ->  Close Long | ||||
|             Action: Neutral, position: Short -> Close Short | ||||
|  | ||||
|             Action: Long, position: Neutral -> Open Long | ||||
|             Action: Long, position: Short -> Close Short and Open Long | ||||
|  | ||||
|             Action: Short, position: Neutral -> Open Short | ||||
|             Action: Short, position: Long -> Close Long and Open Short | ||||
|             """ | ||||
|  | ||||
|             if action == Actions.Neutral.value: | ||||
|                 self._position = Positions.Neutral | ||||
| @@ -69,16 +59,16 @@ class Base4ActionRLEnv(BaseEnvironment): | ||||
|                 self._last_trade_tick = None | ||||
|             elif action == Actions.Long_enter.value: | ||||
|                 self._position = Positions.Long | ||||
|                 trade_type = "long" | ||||
|                 trade_type = "enter_long" | ||||
|                 self._last_trade_tick = self._current_tick | ||||
|             elif action == Actions.Short_enter.value: | ||||
|                 self._position = Positions.Short | ||||
|                 trade_type = "short" | ||||
|                 trade_type = "enter_short" | ||||
|                 self._last_trade_tick = self._current_tick | ||||
|             elif action == Actions.Exit.value: | ||||
|                 self._update_total_profit() | ||||
|                 self._position = Positions.Neutral | ||||
|                 trade_type = "neutral" | ||||
|                 trade_type = "exit" | ||||
|                 self._last_trade_tick = None | ||||
|             else: | ||||
|                 print("case not defined") | ||||
| @@ -86,7 +76,7 @@ class Base4ActionRLEnv(BaseEnvironment): | ||||
|             if trade_type is not None: | ||||
|                 self.trade_history.append( | ||||
|                     {'price': self.current_price(), 'index': self._current_tick, | ||||
|                      'type': trade_type}) | ||||
|                      'type': trade_type, 'profit': self.get_unrealized_profit()}) | ||||
|  | ||||
|         if (self._total_profit < self.max_drawdown or | ||||
|                 self._total_unrealized_profit < self.max_drawdown): | ||||
|   | ||||
| @@ -49,20 +49,10 @@ 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): | ||||
|             """ | ||||
|             Action: Neutral, position: Long ->  Close Long | ||||
|             Action: Neutral, position: Short -> Close Short | ||||
|  | ||||
|             Action: Long, position: Neutral -> Open Long | ||||
|             Action: Long, position: Short -> Close Short and Open Long | ||||
|  | ||||
|             Action: Short, position: Neutral -> Open Short | ||||
|             Action: Short, position: Long -> Close Long and Open Short | ||||
|             """ | ||||
|  | ||||
|             if action == Actions.Neutral.value: | ||||
|                 self._position = Positions.Neutral | ||||
| @@ -70,21 +60,21 @@ class Base5ActionRLEnv(BaseEnvironment): | ||||
|                 self._last_trade_tick = None | ||||
|             elif action == Actions.Long_enter.value: | ||||
|                 self._position = Positions.Long | ||||
|                 trade_type = "long" | ||||
|                 trade_type = "enter_long" | ||||
|                 self._last_trade_tick = self._current_tick | ||||
|             elif action == Actions.Short_enter.value: | ||||
|                 self._position = Positions.Short | ||||
|                 trade_type = "short" | ||||
|                 trade_type = "enter_short" | ||||
|                 self._last_trade_tick = self._current_tick | ||||
|             elif action == Actions.Long_exit.value: | ||||
|                 self._update_total_profit() | ||||
|                 self._position = Positions.Neutral | ||||
|                 trade_type = "neutral" | ||||
|                 trade_type = "exit_long" | ||||
|                 self._last_trade_tick = None | ||||
|             elif action == Actions.Short_exit.value: | ||||
|                 self._update_total_profit() | ||||
|                 self._position = Positions.Neutral | ||||
|                 trade_type = "neutral" | ||||
|                 trade_type = "exit_short" | ||||
|                 self._last_trade_tick = None | ||||
|             else: | ||||
|                 print("case not defined") | ||||
| @@ -92,7 +82,7 @@ class Base5ActionRLEnv(BaseEnvironment): | ||||
|             if trade_type is not None: | ||||
|                 self.trade_history.append( | ||||
|                     {'price': self.current_price(), 'index': self._current_tick, | ||||
|                      'type': trade_type}) | ||||
|                      'type': trade_type, 'profit': self.get_unrealized_profit()}) | ||||
|  | ||||
|         if (self._total_profit < self.max_drawdown or | ||||
|                 self._total_unrealized_profit < self.max_drawdown): | ||||
|   | ||||
| @@ -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 = {} | ||||
|   | ||||
| @@ -114,6 +114,7 @@ class BaseReinforcementLearningModel(IFreqaiModel): | ||||
|  | ||||
|         # normalize all data based on train_dataset only | ||||
|         prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk) | ||||
|  | ||||
|         data_dictionary = dk.normalize_data(data_dictionary) | ||||
|  | ||||
|         # data cleaning/analysis | ||||
| @@ -148,12 +149,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): | ||||
|  | ||||
|         env_info = self.pack_env_dict(dk.pair) | ||||
|  | ||||
|         self.train_env = self.MyRLEnv(df=train_df, | ||||
|                                       prices=prices_train, | ||||
|                                       **env_info) | ||||
|         self.eval_env = Monitor(self.MyRLEnv(df=test_df, | ||||
|                                              prices=prices_test, | ||||
|                                              **env_info)) | ||||
|         self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, **env_info) | ||||
|         self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test, **env_info)) | ||||
|         self.eval_callback = EvalCallback(self.eval_env, deterministic=True, | ||||
|                                           render=False, eval_freq=len(train_df), | ||||
|                                           best_model_save_path=str(dk.data_path)) | ||||
| @@ -238,6 +235,9 @@ class BaseReinforcementLearningModel(IFreqaiModel): | ||||
|         filtered_dataframe, _ = dk.filter_features( | ||||
|             unfiltered_df, dk.training_features_list, training_filter=False | ||||
|         ) | ||||
|  | ||||
|         filtered_dataframe = self.drop_ohlc_from_df(filtered_dataframe, dk) | ||||
|  | ||||
|         filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) | ||||
|         dk.data_dictionary["prediction_features"] = filtered_dataframe | ||||
|  | ||||
| @@ -285,7 +285,6 @@ class BaseReinforcementLearningModel(IFreqaiModel): | ||||
|         train_df = data_dictionary["train_features"] | ||||
|         test_df = data_dictionary["test_features"] | ||||
|  | ||||
|         # %-raw_volume_gen_shift-2_ETH/USDT_1h | ||||
|         # price data for model training and evaluation | ||||
|         tf = self.config['timeframe'] | ||||
|         rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low', | ||||
| @@ -318,8 +317,24 @@ class BaseReinforcementLearningModel(IFreqaiModel): | ||||
|         prices_test.rename(columns=rename_dict, inplace=True) | ||||
|         prices_test.reset_index(drop=True) | ||||
|  | ||||
|         train_df = self.drop_ohlc_from_df(train_df, dk) | ||||
|         test_df = self.drop_ohlc_from_df(test_df, dk) | ||||
|  | ||||
|         return prices_train, prices_test | ||||
|  | ||||
|     def drop_ohlc_from_df(self, df: DataFrame, dk: FreqaiDataKitchen): | ||||
|         """ | ||||
|         Given a dataframe, drop the ohlc data | ||||
|         """ | ||||
|         drop_list = ['%-raw_open', '%-raw_low', '%-raw_high', '%-raw_close'] | ||||
|  | ||||
|         if self.rl_config["drop_ohlc_from_features"]: | ||||
|             df.drop(drop_list, axis=1, inplace=True) | ||||
|             feature_list = dk.training_features_list | ||||
|             dk.training_features_list = [e for e in feature_list if e not in drop_list] | ||||
|  | ||||
|         return df | ||||
|  | ||||
|     def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any: | ||||
|         """ | ||||
|         Can be used by user if they are trying to limit_ram_usage *and* | ||||
|   | ||||
| @@ -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." | ||||
|             ) | ||||
|  | ||||
|   | ||||
| @@ -105,6 +105,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) | ||||
|  | ||||
| @@ -154,8 +158,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 | ||||
| @@ -339,13 +342,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() | ||||
|   | ||||
| @@ -21,7 +21,8 @@ from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, | ||||
|                              State, TradingMode) | ||||
| from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, | ||||
|                                   InvalidOrderException, PricingError) | ||||
| from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds | ||||
| from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date, | ||||
|                                 timeframe_to_seconds) | ||||
| from freqtrade.misc import safe_value_fallback, safe_value_fallback2 | ||||
| from freqtrade.mixins import LoggingMixin | ||||
| from freqtrade.persistence import Order, PairLocks, Trade, init_db | ||||
| @@ -30,6 +31,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 | ||||
| @@ -133,13 +136,13 @@ class FreqtradeBot(LoggingMixin): | ||||
|         # Initialize protections AFTER bot start - otherwise parameters are not loaded. | ||||
|         self.protections = ProtectionManager(self.config, self.strategy.protections) | ||||
|  | ||||
|     def notify_status(self, msg: str) -> None: | ||||
|     def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None: | ||||
|         """ | ||||
|         Public method for users of this class (worker, etc.) to send notifications | ||||
|         via RPC about changes in the bot status. | ||||
|         """ | ||||
|         self.rpc.send_msg({ | ||||
|             'type': RPCMessageType.STATUS, | ||||
|             'type': msg_type, | ||||
|             'status': msg | ||||
|         }) | ||||
|  | ||||
| @@ -212,7 +215,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) | ||||
|  | ||||
| @@ -586,7 +590,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|  | ||||
|         min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair, | ||||
|                                                                   current_entry_rate, | ||||
|                                                                   self.strategy.stoploss) | ||||
|                                                                   0.0) | ||||
|         min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair, | ||||
|                                                                  current_exit_rate, | ||||
|                                                                  self.strategy.stoploss) | ||||
| @@ -594,7 +598,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, | ||||
| @@ -700,7 +704,8 @@ class FreqtradeBot(LoggingMixin): | ||||
|         pos_adjust = trade is not None | ||||
|  | ||||
|         enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( | ||||
|             pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_) | ||||
|             pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_, | ||||
|             pos_adjust) | ||||
|  | ||||
|         if not stake_amount: | ||||
|             return False | ||||
| @@ -809,6 +814,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 | ||||
| @@ -818,7 +826,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 | ||||
| @@ -846,11 +854,13 @@ class FreqtradeBot(LoggingMixin): | ||||
|                 logger.info(f"Canceling stoploss on exchange for {trade}") | ||||
|                 co = self.exchange.cancel_stoploss_order_with_result( | ||||
|                     trade.stoploss_order_id, trade.pair, trade.amount) | ||||
|                 trade.update_order(co) | ||||
|                 self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) | ||||
|  | ||||
|                 # 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( | ||||
| @@ -860,7 +870,12 @@ class FreqtradeBot(LoggingMixin): | ||||
|         trade: Optional[Trade], | ||||
|         order_adjust: bool, | ||||
|         leverage_: Optional[float], | ||||
|         pos_adjust: bool, | ||||
|     ) -> Tuple[float, float, float]: | ||||
|         """ | ||||
|         Validate and eventually adjust (within limits) limit, amount and leverage | ||||
|         :return: Tuple with (price, amount, leverage) | ||||
|         """ | ||||
|  | ||||
|         if price: | ||||
|             enter_limit_requested = price | ||||
| @@ -906,7 +921,9 @@ class FreqtradeBot(LoggingMixin): | ||||
|         # We do however also need min-stake to determine leverage, therefore this is ignored as | ||||
|         # edge-case for now. | ||||
|         min_stake_amount = self.exchange.get_min_pair_stake_amount( | ||||
|             pair, enter_limit_requested, self.strategy.stoploss, leverage) | ||||
|             pair, enter_limit_requested, | ||||
|             self.strategy.stoploss if not pos_adjust else 0.0, | ||||
|             leverage) | ||||
|         max_stake_amount = self.exchange.get_max_pair_stake_amount( | ||||
|             pair, enter_limit_requested, leverage) | ||||
|  | ||||
| @@ -930,12 +947,11 @@ class FreqtradeBot(LoggingMixin): | ||||
|  | ||||
|         return enter_limit_requested, stake_amount, leverage | ||||
|  | ||||
|     def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None, | ||||
|     def _notify_enter(self, trade: Trade, order: Order, order_type: str, | ||||
|                       fill: bool = False, sub_trade: bool = False) -> None: | ||||
|         """ | ||||
|         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: | ||||
| @@ -946,9 +962,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(), | ||||
| @@ -960,6 +976,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(), | ||||
| @@ -978,7 +995,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, | ||||
| @@ -990,7 +1007,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, | ||||
| @@ -1013,12 +1032,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 | ||||
| @@ -1122,8 +1145,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|             trade.stoploss_order_id = None | ||||
|             logger.error(f'Unable to place a stoploss order on exchange. {e}') | ||||
|             logger.warning('Exiting the trade forcefully') | ||||
|             self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( | ||||
|                 exit_type=ExitType.EMERGENCY_EXIT)) | ||||
|             self.emergency_exit(trade, stop_price) | ||||
|  | ||||
|         except ExchangeError: | ||||
|             trade.stoploss_order_id = None | ||||
| @@ -1151,7 +1173,8 @@ class FreqtradeBot(LoggingMixin): | ||||
|             logger.warning('Unable to fetch stoploss order: %s', exception) | ||||
|  | ||||
|         if stoploss_order: | ||||
|             trade.update_order(stoploss_order) | ||||
|             self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, | ||||
|                                     stoploss_order=True) | ||||
|  | ||||
|         # We check if stoploss order is fulfilled | ||||
|         if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): | ||||
| @@ -1215,7 +1238,9 @@ class FreqtradeBot(LoggingMixin): | ||||
|         :param order: Current on exchange stoploss order | ||||
|         :return: None | ||||
|         """ | ||||
|         stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation) | ||||
|         stoploss_norm = self.exchange.price_to_precision( | ||||
|             trade.pair, trade.stoploss_or_liquidation, | ||||
|             rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP) | ||||
|  | ||||
|         if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side): | ||||
|             # we check if the update is necessary | ||||
| @@ -1225,13 +1250,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): | ||||
| @@ -1281,13 +1301,16 @@ class FreqtradeBot(LoggingMixin): | ||||
|             if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: | ||||
|                 logger.warning(f'Emergency exiting trade {trade}, as the exit order ' | ||||
|                                f'timed out {max_timeouts} times.') | ||||
|                 try: | ||||
|                     self.execute_trade_exit( | ||||
|                         trade, order['price'], | ||||
|                         exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) | ||||
|                 except DependencyException as exception: | ||||
|                     logger.warning( | ||||
|                         f'Unable to emergency sell trade {trade.pair}: {exception}') | ||||
|                 self.emergency_exit(trade, order['price']) | ||||
|  | ||||
|     def emergency_exit(self, trade: Trade, price: float) -> None: | ||||
|         try: | ||||
|             self.execute_trade_exit( | ||||
|                 trade, price, | ||||
|                 exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) | ||||
|         except DependencyException as exception: | ||||
|             logger.warning( | ||||
|                 f'Unable to emergency exit trade {trade.pair}: {exception}') | ||||
|  | ||||
|     def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None: | ||||
|         """ | ||||
| @@ -1460,35 +1483,34 @@ class FreqtradeBot(LoggingMixin): | ||||
|                     return False | ||||
|  | ||||
|             try: | ||||
|                 co = self.exchange.cancel_order_with_result(order['id'], trade.pair, | ||||
|                                                             trade.amount) | ||||
|                 order = self.exchange.cancel_order_with_result( | ||||
|                     order['id'], trade.pair, trade.amount) | ||||
|             except InvalidOrderException: | ||||
|                 logger.exception( | ||||
|                     f"Could not cancel {trade.exit_side} order {trade.open_order_id}") | ||||
|                 return False | ||||
|             trade.close_rate = None | ||||
|             trade.close_rate_requested = None | ||||
|             trade.close_profit = None | ||||
|             trade.close_profit_abs = None | ||||
|  | ||||
|             # Set exit_reason for fill message | ||||
|             exit_reason_prev = trade.exit_reason | ||||
|             trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason | ||||
|             self.update_trade_state(trade, trade.open_order_id, co) | ||||
|             # Order might be filled above in odd timing issues. | ||||
|             if co.get('status') in ('canceled', 'cancelled'): | ||||
|             if order.get('status') in ('canceled', 'cancelled'): | ||||
|                 trade.exit_reason = None | ||||
|                 trade.open_order_id = None | ||||
|             else: | ||||
|                 trade.exit_reason = exit_reason_prev | ||||
|  | ||||
|             logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') | ||||
|             cancelled = True | ||||
|         else: | ||||
|             reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] | ||||
|             logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') | ||||
|             self.update_trade_state(trade, trade.open_order_id, order) | ||||
|             trade.exit_reason = None | ||||
|             trade.open_order_id = None | ||||
|  | ||||
|         self.update_trade_state(trade, trade.open_order_id, order) | ||||
|  | ||||
|         logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') | ||||
|         trade.close_rate = None | ||||
|         trade.close_rate_requested = None | ||||
|  | ||||
|         self._notify_exit_cancel( | ||||
|             trade, | ||||
|             order_type=self.strategy.order_types['exit'], | ||||
| @@ -1651,7 +1673,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, | ||||
| @@ -1677,6 +1699,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, | ||||
| @@ -1707,7 +1730,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(), | ||||
| @@ -1729,6 +1752,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, | ||||
| @@ -1760,11 +1784,11 @@ class FreqtradeBot(LoggingMixin): | ||||
|             return False | ||||
|  | ||||
|         # Update trade with order values | ||||
|         logger.info(f'Found open order for {trade}') | ||||
|         if not stoploss_order: | ||||
|             logger.info(f'Found open order for {trade}') | ||||
|         try: | ||||
|             order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, | ||||
|                                                                                 trade.pair, | ||||
|                                                                                 stoploss_order) | ||||
|             order = action_order or self.exchange.fetch_order_or_stoploss_order( | ||||
|                 order_id, trade.pair, stoploss_order) | ||||
|         except InvalidOrderException as exception: | ||||
|             logger.warning('Unable to fetch order %s: %s', order_id, exception) | ||||
|             return False | ||||
| @@ -1793,7 +1817,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|                     # TODO: should shorting/leverage be supported by Edge, | ||||
|                     # then this will need to be fixed. | ||||
|                     trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) | ||||
|             if order.get('side') == trade.entry_side or trade.amount > 0: | ||||
|             if order.get('side') == trade.entry_side or (trade.amount > 0 and trade.is_open): | ||||
|                 # Must also run for partial exits | ||||
|                 # TODO: Margin will need to use interest_rate as well. | ||||
|                 # interest_rate = self.exchange.get_interest_rate() | ||||
| @@ -1829,21 +1853,27 @@ class FreqtradeBot(LoggingMixin): | ||||
|                 self.handle_protections(trade.pair, trade.trade_direction) | ||||
|         elif send_msg and not trade.open_order_id and not stoploss_order: | ||||
|             # Enter fill | ||||
|             self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) | ||||
|             self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade) | ||||
|  | ||||
|     def handle_protections(self, pair: str, side: LongShort) -> None: | ||||
|         # Lock pair for one candle to prevent immediate rebuys | ||||
|         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, | ||||
|   | ||||
| @@ -6,8 +6,7 @@ import logging | ||||
| import re | ||||
| from datetime import datetime | ||||
| from pathlib import Path | ||||
| from typing import Any, Dict, Iterator, List, Mapping, Optional, Union | ||||
| from typing.io import IO | ||||
| from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| import orjson | ||||
| @@ -103,7 +102,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: | ||||
|     logger.debug(f'done joblib dump to "{filename}"') | ||||
|  | ||||
|  | ||||
| def json_load(datafile: IO) -> Any: | ||||
| def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any: | ||||
|     """ | ||||
|     load data with rapidjson | ||||
|     Use this to have a consistent experience, | ||||
|   | ||||
| @@ -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,12 +741,12 @@ 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) | ||||
|  | ||||
|         min_stake_amount = self.exchange.get_min_pair_stake_amount( | ||||
|             pair, propose_rate, -0.05, leverage=leverage) or 0 | ||||
|             pair, propose_rate, -0.05 if not pos_adjust else 0.0, leverage=leverage) or 0 | ||||
|         max_stake_amount = self.exchange.get_max_pair_stake_amount( | ||||
|             pair, propose_rate, leverage=leverage) | ||||
|         stake_available = self.wallets.get_available_stake_amount() | ||||
| @@ -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 | ||||
| @@ -24,6 +23,8 @@ logger = logging.getLogger(__name__) | ||||
|  | ||||
| NON_OPT_PARAM_APPENDIX = "  # value loaded from strategy" | ||||
|  | ||||
| HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN | ||||
|  | ||||
|  | ||||
| def hyperopt_serializer(x): | ||||
|     if isinstance(x, np.integer): | ||||
| @@ -77,9 +78,18 @@ class HyperoptTools(): | ||||
|         with filename.open('w') as f: | ||||
|             rapidjson.dump(final_params, f, indent=2, | ||||
|                            default=hyperopt_serializer, | ||||
|                            number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN | ||||
|                            number_mode=HYPER_PARAMS_FILE_FORMAT | ||||
|                            ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def load_params(filename: Path) -> Dict: | ||||
|         """ | ||||
|         Load parameters from file | ||||
|         """ | ||||
|         with filename.open('r') as f: | ||||
|             params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT) | ||||
|         return params | ||||
|  | ||||
|     @staticmethod | ||||
|     def try_export_params(config: Config, strategy_name: str, params: Dict): | ||||
|         if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False): | ||||
| @@ -190,7 +200,7 @@ class HyperoptTools(): | ||||
|             for s in ['buy', 'sell', 'protection', | ||||
|                       'roi', 'stoploss', 'trailing', 'max_open_trades']: | ||||
|                 HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) | ||||
|             print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) | ||||
|             print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT)) | ||||
|  | ||||
|         else: | ||||
|             HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", | ||||
| @@ -464,8 +474,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,9 +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, mapped_column | ||||
| from sqlalchemy.orm.scoping import _QueryDescriptorType | ||||
| 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 | ||||
| @@ -14,8 +13,7 @@ class PairLock(ModelBase): | ||||
|     Pair Locks database model. | ||||
|     """ | ||||
|     __tablename__ = 'pairlocks' | ||||
|     query: ClassVar[_QueryDescriptorType] | ||||
|     _session: ClassVar[SessionType] | ||||
|     session: ClassVar[SessionType] | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
| @@ -38,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 | ||||
| @@ -54,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,17 +5,18 @@ 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, lazyload, mapped_column, relationship | ||||
| from sqlalchemy.orm.scoping import _QueryDescriptorType | ||||
| 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) | ||||
| from freqtrade.enums import ExitType, TradingMode | ||||
| from freqtrade.exceptions import DependencyException, OperationalException | ||||
| from freqtrade.exchange import amount_to_contract_precision, price_to_precision | ||||
| from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, | ||||
|                                 price_to_precision) | ||||
| from freqtrade.leverage import interest | ||||
| from freqtrade.persistence.base import ModelBase, SessionType | ||||
| from freqtrade.util import FtPrecise | ||||
| @@ -36,8 +37,7 @@ class Order(ModelBase): | ||||
|     Mirrors CCXT Order structure | ||||
|     """ | ||||
|     __tablename__ = 'orders' | ||||
|     query: ClassVar[_QueryDescriptorType] | ||||
|     _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 +263,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 +276,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 +561,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, | ||||
|         } | ||||
|  | ||||
| @@ -595,7 +598,8 @@ class LocalTrade(): | ||||
|         """ | ||||
|         Method used internally to set self.stop_loss. | ||||
|         """ | ||||
|         stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode) | ||||
|         stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode, | ||||
|                                             rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) | ||||
|         if not self.stop_loss: | ||||
|             self.initial_stop_loss = stop_loss_norm | ||||
|         self.stop_loss = stop_loss_norm | ||||
| @@ -626,7 +630,8 @@ class LocalTrade(): | ||||
|         if self.initial_stop_loss_pct is None or refresh: | ||||
|             self.__set_stop_loss(new_loss, stoploss) | ||||
|             self.initial_stop_loss = price_to_precision( | ||||
|                 new_loss, self.price_precision, self.precision_mode) | ||||
|                 new_loss, self.price_precision, self.precision_mode, | ||||
|                 rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) | ||||
|             self.initial_stop_loss_pct = -1 * abs(stoploss) | ||||
|  | ||||
|         # evaluate if the stop loss needs to be updated | ||||
| @@ -690,21 +695,24 @@ class LocalTrade(): | ||||
|             else: | ||||
|                 logger.warning( | ||||
|                     f'Got different open_order_id {self.open_order_id} != {order.order_id}') | ||||
|  | ||||
|         elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): | ||||
|             self.stoploss_order_id = None | ||||
|             self.close_rate_requested = self.stop_loss | ||||
|             self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value | ||||
|             if self.is_open: | ||||
|                 logger.info(f'{order.order_type.upper()} is hit for {self}.') | ||||
|         else: | ||||
|             raise ValueError(f'Unknown order type: {order.order_type}') | ||||
|  | ||||
|         if order.ft_order_side != self.entry_side: | ||||
|             amount_tr = amount_to_contract_precision(self.amount, self.amount_precision, | ||||
|                                                      self.precision_mode, self.contract_size) | ||||
|             if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC): | ||||
|                 self.close(order.safe_price) | ||||
|             else: | ||||
|                 self.recalc_trade_from_orders() | ||||
|         elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'): | ||||
|             self.stoploss_order_id = None | ||||
|             self.close_rate_requested = self.stop_loss | ||||
|             self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value | ||||
|             if self.is_open: | ||||
|                 logger.info(f'{order.order_type.upper()} is hit for {self}.') | ||||
|             self.close(order.safe_price) | ||||
|         else: | ||||
|             raise ValueError(f'Unknown order type: {order.order_type}') | ||||
|  | ||||
|         Trade.commit() | ||||
|  | ||||
|     def close(self, rate: float, *, show_msg: bool = True) -> None: | ||||
| @@ -1088,6 +1096,11 @@ class LocalTrade(): | ||||
|         In live mode, converts the filter to a database query and returns all rows | ||||
|         In Backtest mode, uses filters on Trade.trades to get the result. | ||||
|  | ||||
|         :param pair: Filter by pair | ||||
|         :param is_open: Filter by open/closed status | ||||
|         :param open_date: Filter by open_date (filters via trade.open_date > input) | ||||
|         :param close_date: Filter by close_date (filters via trade.close_date > input) | ||||
|                            Will implicitly only return closed trades. | ||||
|         :return: unsorted List[Trade] | ||||
|         """ | ||||
|  | ||||
| @@ -1148,7 +1161,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 | ||||
|  | ||||
| @@ -1181,8 +1196,7 @@ class Trade(ModelBase, LocalTrade): | ||||
|     Note: Fields must be aligned with LocalTrade class | ||||
|     """ | ||||
|     __tablename__ = 'trades' | ||||
|     query: ClassVar[_QueryDescriptorType] | ||||
|     _session: ClassVar[SessionType] | ||||
|     session: ClassVar[SessionType] | ||||
|  | ||||
|     use_db: bool = True | ||||
|  | ||||
| @@ -1282,18 +1296,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, | ||||
| @@ -1327,7 +1341,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. | ||||
| @@ -1342,22 +1356,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(): | ||||
| @@ -1387,11 +1414,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 | ||||
| @@ -1401,8 +1429,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)) | ||||
| @@ -1418,15 +1447,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, | ||||
| @@ -1451,15 +1483,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 [ | ||||
|             { | ||||
| @@ -1483,16 +1516,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 [ | ||||
|             { | ||||
| @@ -1516,18 +1549,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: | ||||
| @@ -1563,11 +1596,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 | ||||
| @@ -1577,12 +1614,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 | ||||
| @@ -1631,8 +1669,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'] | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from typing import Any, Dict, Optional | ||||
|  | ||||
| from freqtrade.constants import Config | ||||
| from freqtrade.exceptions import OperationalException | ||||
| from freqtrade.exchange import ROUND_UP | ||||
| from freqtrade.exchange.types import Ticker | ||||
| from freqtrade.plugins.pairlist.IPairList import IPairList | ||||
|  | ||||
| @@ -61,9 +62,10 @@ class PrecisionFilter(IPairList): | ||||
|         stop_price = ticker['last'] * self._stoploss | ||||
|  | ||||
|         # Adjust stop-prices to precision | ||||
|         sp = self._exchange.price_to_precision(pair, stop_price) | ||||
|         sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP) | ||||
|  | ||||
|         stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99) | ||||
|         stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99, | ||||
|                                                            rounding_mode=ROUND_UP) | ||||
|         logger.debug(f"{pair} - {sp} : {stop_gap_price}") | ||||
|  | ||||
|         if sp <= stop_gap_price: | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import logging | ||||
| from typing import Any, Dict, Optional | ||||
|  | ||||
| from freqtrade.constants import Config | ||||
| from freqtrade.exceptions import OperationalException | ||||
| from freqtrade.exchange.types import Ticker | ||||
| from freqtrade.plugins.pairlist.IPairList import IPairList | ||||
|  | ||||
| @@ -22,6 +23,12 @@ class SpreadFilter(IPairList): | ||||
|         self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005) | ||||
|         self._enabled = self._max_spread_ratio != 0 | ||||
|  | ||||
|         if not self._exchange.get_option('tickers_have_bid_ask'): | ||||
|             raise OperationalException( | ||||
|                 f"{self.name} requires exchange to have bid/ask data for tickers, " | ||||
|                 "which is not available for the selected exchange / trading mode." | ||||
|             ) | ||||
|  | ||||
|     @property | ||||
|     def needstickers(self) -> bool: | ||||
|         """ | ||||
|   | ||||
| @@ -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] | ||||
| @@ -286,6 +290,7 @@ class OpenTradeSchema(TradeSchema): | ||||
|     current_rate: float | ||||
|     total_profit_abs: float | ||||
|     total_profit_fiat: Optional[float] | ||||
|     total_profit_ratio: Optional[float] | ||||
|  | ||||
|     open_order: Optional[str] | ||||
|  | ||||
| @@ -310,7 +315,7 @@ class LockModel(BaseModel): | ||||
|     lock_timestamp: int | ||||
|     pair: str | ||||
|     side: str | ||||
|     reason: str | ||||
|     reason: Optional[str] | ||||
|  | ||||
|  | ||||
| class Locks(BaseModel): | ||||
|   | ||||
| @@ -42,7 +42,8 @@ logger = logging.getLogger(__name__) | ||||
| # 2.22: Add FreqAI to backtesting | ||||
| # 2.23: Allow plot config request in webserver mode | ||||
| # 2.24: Add cancel_open_order endpoint | ||||
| API_VERSION = 2.24 | ||||
| # 2.25: Add several profit values to /status endpoint | ||||
| API_VERSION = 2.25 | ||||
|  | ||||
| # Public API, requires no auth. | ||||
| router_public = APIRouter() | ||||
|   | ||||
| @@ -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') | ||||
|  | ||||
|   | ||||
| @@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server): | ||||
|  | ||||
|     @contextlib.contextmanager | ||||
|     def run_in_thread(self): | ||||
|         self.thread = threading.Thread(target=self.run) | ||||
|         self.thread = threading.Thread(target=self.run, name='FTUvicorn') | ||||
|         self.thread.start() | ||||
|         while not self.started: | ||||
|             time.sleep(1e-3) | ||||
|   | ||||
| @@ -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() | ||||
|  | ||||
| @@ -192,6 +195,11 @@ class RPC: | ||||
|                     current_profit = trade.close_profit or 0.0 | ||||
|                     current_profit_abs = trade.close_profit_abs or 0.0 | ||||
|                 total_profit_abs = trade.realized_profit + current_profit_abs | ||||
|                 total_profit_ratio: Optional[float] = None | ||||
|                 if trade.max_stake_amount: | ||||
|                     total_profit_ratio = ( | ||||
|                         (total_profit_abs / trade.max_stake_amount) * trade.leverage | ||||
|                     ) | ||||
|  | ||||
|                 # Calculate fiat profit | ||||
|                 if not isnan(current_profit_abs) and self._fiat_converter: | ||||
| @@ -224,6 +232,7 @@ class RPC: | ||||
|  | ||||
|                     total_profit_abs=total_profit_abs, | ||||
|                     total_profit_fiat=total_profit_fiat, | ||||
|                     total_profit_ratio=total_profit_ratio, | ||||
|                     stoploss_current_dist=stoploss_current_dist, | ||||
|                     stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), | ||||
|                     stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), | ||||
| @@ -333,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) | ||||
| @@ -375,19 +386,25 @@ 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.session.scalar( | ||||
|             select(func.count(Trade.id)).filter(Trade.is_open.is_(False))) | ||||
|  | ||||
|         return { | ||||
|             "trades": output, | ||||
|             "trades_count": len(output), | ||||
|             "offset": offset, | ||||
|             "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), | ||||
|             "total_trades": total_trades, | ||||
|         } | ||||
|  | ||||
|     def _rpc_stats(self) -> Dict[str, Any]: | ||||
| @@ -429,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 = [] | ||||
| @@ -939,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: str | ||||
|     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 | ||||
|  | ||||
| @@ -414,6 +417,9 @@ class Telegram(RPCHandler): | ||||
|  | ||||
|         elif msg_type == RPCMessageType.WARNING: | ||||
|             message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`" | ||||
|         elif msg_type == RPCMessageType.EXCEPTION: | ||||
|             # Errors will contain exceptions, which are wrapped in tripple ticks. | ||||
|             message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}" | ||||
|  | ||||
|         elif msg_type == RPCMessageType.STARTUP: | ||||
|             message = f"{msg['status']}" | ||||
| @@ -424,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 | ||||
| @@ -448,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')) | ||||
|  | ||||
| @@ -510,14 +516,14 @@ class Telegram(RPCHandler): | ||||
|                 if prev_avg_price: | ||||
|                     minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price | ||||
|  | ||||
|                 lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg profit") | ||||
|                 lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg Profit") | ||||
|                 if is_open: | ||||
|                     lines.append("({})".format(cur_entry_datetime | ||||
|                                                .humanize(granularity=["day", "hour", "minute"]))) | ||||
|                 lines.append(f"*Amount:* {cur_entry_amount} " | ||||
|                              f"({round_coin_value(order['cost'], quote_currency)})") | ||||
|                 lines.append(f"*Average {wording} Price:* {cur_entry_average} " | ||||
|                              f"({price_to_1st_entry:.2%} from 1st entry rate)") | ||||
|                              f"({price_to_1st_entry:.2%} from 1st entry Rate)") | ||||
|                 lines.append(f"*Order filled:* {order['order_filled_date']}") | ||||
|  | ||||
|                 # TODO: is this really useful? | ||||
| @@ -569,6 +575,8 @@ class Telegram(RPCHandler): | ||||
|                                  and not o['ft_order_side'] == 'stoploss']) | ||||
|             r['exit_reason'] = r.get('exit_reason', "") | ||||
|             r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency']) | ||||
|             r['max_stake_amount_r'] = round_coin_value( | ||||
|                 r['max_stake_amount'] or r['stake_amount'], r['quote_currency']) | ||||
|             r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency']) | ||||
|             r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency']) | ||||
|             r['total_profit_abs_r'] = round_coin_value( | ||||
| @@ -580,31 +588,37 @@ class Telegram(RPCHandler): | ||||
|                 f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}" | ||||
|                 + " ` ({leverage}x)`" if r.get('leverage') else "", | ||||
|                 "*Amount:* `{amount} ({stake_amount_r})`", | ||||
|                 "*Total invested:* `{max_stake_amount_r}`" if position_adjust else "", | ||||
|                 "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", | ||||
|                 "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", | ||||
|             ] | ||||
|  | ||||
|             if position_adjust: | ||||
|                 max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") | ||||
|                 lines.append("*Number of Entries:* `{num_entries}" + max_buy_str + "`") | ||||
|                 lines.append("*Number of Exits:* `{num_exits}`") | ||||
|                 lines.extend([ | ||||
|                     "*Number of Entries:* `{num_entries}" + max_buy_str + "`", | ||||
|                     "*Number of Exits:* `{num_exits}`" | ||||
|                 ]) | ||||
|  | ||||
|             lines.extend([ | ||||
|                 "*Open Rate:* `{open_rate:.8f}`", | ||||
|                 "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", | ||||
|                 "*Open Date:* `{open_date}`", | ||||
|                 "*Close Date:* `{close_date}`" if r['close_date'] else "", | ||||
|                 "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", | ||||
|                 " \n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", | ||||
|                 ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") | ||||
|                 + "`{profit_ratio:.2%}` `({profit_abs_r})`", | ||||
|             ]) | ||||
|  | ||||
|             if r['is_open']: | ||||
|                 if r.get('realized_profit'): | ||||
|                     lines.append( | ||||
|                         "*Realized Profit:* `{realized_profit_r} {realized_profit_ratio:.2%}`") | ||||
|                     lines.append("*Total Profit:* `{total_profit_abs_r}` ") | ||||
|                     lines.extend([ | ||||
|                         "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`", | ||||
|                         "*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`" | ||||
|                     ]) | ||||
|  | ||||
|                 # Append empty line to improve readability | ||||
|                 lines.append(" ") | ||||
|                 if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] | ||||
|                         and r['initial_stop_loss_ratio'] is not None): | ||||
|                     # Adding initial stoploss only if it is different from stoploss | ||||
| @@ -1329,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", | ||||
| @@ -1631,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]: | ||||
| @@ -58,6 +59,7 @@ class Webhook(RPCHandler): | ||||
|             valuedict = whconfig.get('webhookexitcancel') | ||||
|         elif msg['type'] in (RPCMessageType.STATUS, | ||||
|                              RPCMessageType.STARTUP, | ||||
|                              RPCMessageType.EXCEPTION, | ||||
|                              RPCMessageType.WARNING): | ||||
|             valuedict = whconfig.get('webhookstatus') | ||||
|         elif msg['type'].value in whconfig: | ||||
| @@ -74,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: | ||||
|  | ||||
| @@ -112,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() | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union | ||||
|  | ||||
| from freqtrade.constants import Config | ||||
| from freqtrade.exceptions import OperationalException | ||||
| from freqtrade.misc import deep_merge_dicts, json_load | ||||
| from freqtrade.misc import deep_merge_dicts | ||||
| from freqtrade.optimize.hyperopt_tools import HyperoptTools | ||||
| from freqtrade.strategy.parameters import BaseParameter | ||||
|  | ||||
| @@ -124,8 +124,7 @@ class HyperStrategyMixin: | ||||
|         if filename.is_file(): | ||||
|             logger.info(f"Loading parameters from file {filename}") | ||||
|             try: | ||||
|                 with filename.open('r') as f: | ||||
|                     params = json_load(f) | ||||
|                 params = HyperoptTools.load_params(filename) | ||||
|                 if params.get('strategy_name') != self.__class__.__name__: | ||||
|                     raise OperationalException('Invalid parameter file provided.') | ||||
|                 return params | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -86,37 +86,41 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, | ||||
| def stoploss_from_open( | ||||
|     open_relative_stop: float, | ||||
|     current_profit: float, | ||||
|     is_short: bool = False | ||||
|     is_short: bool = False, | ||||
|     leverage: float = 1.0 | ||||
| ) -> float: | ||||
|     """ | ||||
|  | ||||
|     Given the current profit, and a desired stop loss value relative to the open price, | ||||
|     Given the current profit, and a desired stop loss value relative to the trade entry price, | ||||
|     return a stop loss value that is relative to the current price, and which can be | ||||
|     returned from `custom_stoploss`. | ||||
|  | ||||
|     The requested stop can be positive for a stop above the open price, or negative for | ||||
|     a stop below the open price. The return value is always >= 0. | ||||
|     `open_relative_stop` will be considered as adjusted for leverage if leverage is provided.. | ||||
|  | ||||
|     Returns 0 if the resulting stop price would be above/below (longs/shorts) the current price | ||||
|  | ||||
|     :param open_relative_stop: Desired stop loss percentage relative to open price | ||||
|     :param open_relative_stop: Desired stop loss percentage, relative to the open price, | ||||
|                                adjusted for leverage | ||||
|     :param current_profit: The current profit percentage | ||||
|     :param is_short: When true, perform the calculation for short instead of long | ||||
|     :param leverage: Leverage to use for the calculation | ||||
|     :return: Stop loss value relative to current price | ||||
|     """ | ||||
|  | ||||
|     # formula is undefined for current_profit -1 (longs) or 1 (shorts), return maximum value | ||||
|     if (current_profit == -1 and not is_short) or (is_short and current_profit == 1): | ||||
|     _current_profit = current_profit / leverage | ||||
|     if (_current_profit == -1 and not is_short) or (is_short and _current_profit == 1): | ||||
|         return 1 | ||||
|  | ||||
|     if is_short is True: | ||||
|         stoploss = -1 + ((1 - open_relative_stop) / (1 - current_profit)) | ||||
|         stoploss = -1 + ((1 - open_relative_stop / leverage) / (1 - _current_profit)) | ||||
|     else: | ||||
|         stoploss = 1 - ((1 + open_relative_stop) / (1 + current_profit)) | ||||
|         stoploss = 1 - ((1 + open_relative_stop / leverage) / (1 + _current_profit)) | ||||
|  | ||||
|     # negative stoploss values indicate the requested stop price is higher/lower | ||||
|     # (long/short) than the current price | ||||
|     return max(stoploss, 0.0) | ||||
|     return max(stoploss * leverage, 0.0) | ||||
|  | ||||
|  | ||||
| def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False) -> float: | ||||
|   | ||||
							
								
								
									
										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) | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import sdnotify | ||||
| from freqtrade import __version__ | ||||
| from freqtrade.configuration import Configuration | ||||
| from freqtrade.constants import PROCESS_THROTTLE_SECS, RETRY_TIMEOUT, Config | ||||
| from freqtrade.enums import State | ||||
| from freqtrade.enums import RPCMessageType, State | ||||
| from freqtrade.exceptions import OperationalException, TemporaryError | ||||
| from freqtrade.exchange import timeframe_to_next_date | ||||
| from freqtrade.freqtradebot import FreqtradeBot | ||||
| @@ -185,7 +185,10 @@ class Worker: | ||||
|             tb = traceback.format_exc() | ||||
|             hint = 'Issue `/start` if you think it is safe to restart.' | ||||
|  | ||||
|             self.freqtrade.notify_status(f'OperationalException:\n```\n{tb}```{hint}') | ||||
|             self.freqtrade.notify_status( | ||||
|                 f'*OperationalException:*\n```\n{tb}```\n {hint}', | ||||
|                 msg_type=RPCMessageType.EXCEPTION | ||||
|             ) | ||||
|  | ||||
|             logger.exception('OperationalException. Stopping trader ...') | ||||
|             self.freqtrade.state = State.STOPPED | ||||
|   | ||||
		Reference in New Issue
	
	Block a user