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