Merge branch 'develop' into pr/SurferAdmin/6916
This commit is contained in:
@@ -24,7 +24,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
|
||||
print_colorized = config.get('print_colorized', False)
|
||||
print_json = config.get('print_json', False)
|
||||
export_csv = config.get('export_csv', None)
|
||||
export_csv = config.get('export_csv')
|
||||
no_details = config.get('hyperopt_list_no_details', False)
|
||||
no_header = False
|
||||
|
||||
|
@@ -129,7 +129,7 @@ class Configuration:
|
||||
# Default to in-memory db for dry_run if not specified
|
||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||
else:
|
||||
if not config.get('db_url', None):
|
||||
if not config.get('db_url'):
|
||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||
logger.info('Dry run is disabled')
|
||||
|
||||
@@ -182,7 +182,7 @@ class Configuration:
|
||||
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
||||
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
||||
|
||||
config.update({'datadir': create_datadir(config, self.args.get('datadir', None))})
|
||||
config.update({'datadir': create_datadir(config, self.args.get('datadir'))})
|
||||
logger.info('Using data directory: %s ...', config.get('datadir'))
|
||||
|
||||
if self.args.get('exportfilename'):
|
||||
@@ -221,7 +221,7 @@ class Configuration:
|
||||
if config.get('max_open_trades') == -1:
|
||||
config['max_open_trades'] = float('inf')
|
||||
|
||||
if self.args.get('stake_amount', None):
|
||||
if self.args.get('stake_amount'):
|
||||
# Convert explicitly to float to support CLI argument for both unlimited and value
|
||||
try:
|
||||
self.args['stake_amount'] = float(self.args['stake_amount'])
|
||||
@@ -474,7 +474,7 @@ class Configuration:
|
||||
configuration instead of the content)
|
||||
"""
|
||||
if (argname in self.args and self.args[argname] is not None
|
||||
and self.args[argname] is not False):
|
||||
and self.args[argname] is not False):
|
||||
|
||||
config.update({argname: self.args[argname]})
|
||||
if logfun:
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@ MAP_EXCHANGE_CHILDCLASS = {
|
||||
'binanceje': 'binance',
|
||||
'binanceusdm': 'binance',
|
||||
'okex': 'okx',
|
||||
'gate': 'gateio',
|
||||
}
|
||||
|
||||
SUPPORTED_EXCHANGES = [
|
||||
@@ -63,17 +64,16 @@ EXCHANGE_HAS_REQUIRED = [
|
||||
'fetchOrder',
|
||||
'cancelOrder',
|
||||
'createOrder',
|
||||
# 'createLimitOrder', 'createMarketOrder',
|
||||
'fetchBalance',
|
||||
|
||||
# Public endpoints
|
||||
'loadMarkets',
|
||||
'fetchOHLCV',
|
||||
]
|
||||
|
||||
EXCHANGE_HAS_OPTIONAL = [
|
||||
# Private
|
||||
'fetchMyTrades', # Trades for order - fee detection
|
||||
# 'createLimitOrder', 'createMarketOrder', # Either OR for orders
|
||||
# 'setLeverage', # Margin/Futures trading
|
||||
# 'setMarginMode', # Margin/Futures trading
|
||||
# 'fetchFundingHistory', # Futures trading
|
||||
|
@@ -77,6 +77,7 @@ class Exchange:
|
||||
"mark_ohlcv_price": "mark",
|
||||
"mark_ohlcv_timeframe": "8h",
|
||||
"ccxt_futures_name": "swap",
|
||||
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||
}
|
||||
_ft_has: Dict = {}
|
||||
@@ -387,7 +388,7 @@ class Exchange:
|
||||
and market.get('base', None) is not None
|
||||
and (self.precisionMode != TICK_SIZE
|
||||
# Too low precision will falsify calculations
|
||||
or market.get('precision', {}).get('price', None) > 1e-11)
|
||||
or market.get('precision', {}).get('price') > 1e-11)
|
||||
and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
|
||||
or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
|
||||
or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
|
||||
@@ -537,7 +538,7 @@ class Exchange:
|
||||
# The internal info array is different for each particular market,
|
||||
# its contents depend on the exchange.
|
||||
# It can also be a string or similar ... so we need to verify that first.
|
||||
elif (isinstance(self.markets[pair].get('info', None), dict)
|
||||
elif (isinstance(self.markets[pair].get('info'), dict)
|
||||
and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
|
||||
# Warn users about restricted pairs in whitelist.
|
||||
# We cannot determine reliably if Users are affected.
|
||||
@@ -585,10 +586,13 @@ class Exchange:
|
||||
"""
|
||||
Checks if order-types configured in strategy/config are supported
|
||||
"""
|
||||
if any(v == 'market' for k, v in order_types.items()):
|
||||
if not self.exchange_has('createMarketOrder'):
|
||||
raise OperationalException(
|
||||
f'Exchange {self.name} does not support market orders.')
|
||||
# TODO: Reenable once ccxt fixes createMarketOrder assignment - as well as
|
||||
# Revert the change in test_validate_ordertypes.
|
||||
|
||||
# if any(v == 'market' for k, v in order_types.items()):
|
||||
# if not self.exchange_has('createMarketOrder'):
|
||||
# raise OperationalException(
|
||||
# f'Exchange {self.name} does not support market orders.')
|
||||
|
||||
if (order_types.get("stoploss_on_exchange")
|
||||
and not self._ft_has.get("stoploss_on_exchange", False)):
|
||||
@@ -1631,27 +1635,35 @@ class Exchange:
|
||||
and order['fee']['cost'] is not None
|
||||
)
|
||||
|
||||
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
|
||||
def calculate_fee_rate(
|
||||
self, fee: Dict, symbol: str, cost: float, amount: float) -> Optional[float]:
|
||||
"""
|
||||
Calculate fee rate if it's not given by the exchange.
|
||||
:param order: Order or trade (one trade) dict
|
||||
:param fee: ccxt Fee dict - must contain cost / currency / rate
|
||||
:param symbol: Symbol of the order
|
||||
:param cost: Total cost of the order
|
||||
:param amount: Amount of the order
|
||||
"""
|
||||
if order['fee'].get('rate') is not None:
|
||||
return order['fee'].get('rate')
|
||||
fee_curr = order['fee']['currency']
|
||||
if fee.get('rate') is not None:
|
||||
return fee.get('rate')
|
||||
fee_curr = fee.get('currency')
|
||||
if fee_curr is None:
|
||||
return None
|
||||
fee_cost = fee['cost']
|
||||
if self._ft_has['fee_cost_in_contracts']:
|
||||
# Convert cost via "contracts" conversion
|
||||
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
|
||||
|
||||
# Calculate fee based on order details
|
||||
if fee_curr in self.get_pair_base_currency(order['symbol']):
|
||||
if fee_curr == self.get_pair_base_currency(symbol):
|
||||
# Base currency - divide by amount
|
||||
return round(
|
||||
order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8)
|
||||
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
|
||||
return round(fee['cost'] / amount, 8)
|
||||
elif fee_curr == self.get_pair_quote_currency(symbol):
|
||||
# Quote currency - divide by cost
|
||||
return round(self._contracts_to_amount(
|
||||
order['symbol'], order['fee']['cost']) / order['cost'],
|
||||
8) if order['cost'] else None
|
||||
return round(fee_cost / cost, 8) if cost else None
|
||||
else:
|
||||
# If Fee currency is a different currency
|
||||
if not order['cost']:
|
||||
if not cost:
|
||||
# If cost is None or 0.0 -> falsy, return None
|
||||
return None
|
||||
try:
|
||||
@@ -1663,19 +1675,28 @@ class Exchange:
|
||||
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
|
||||
if not fee_to_quote_rate:
|
||||
return None
|
||||
return round((self._contracts_to_amount(
|
||||
order['symbol'], order['fee']['cost']) * fee_to_quote_rate) / order['cost'], 8)
|
||||
return round((fee_cost * fee_to_quote_rate) / cost, 8)
|
||||
|
||||
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
||||
def extract_cost_curr_rate(self, fee: Dict, symbol: str, cost: float,
|
||||
amount: float) -> Tuple[float, str, Optional[float]]:
|
||||
"""
|
||||
Extract tuple of cost, currency, rate.
|
||||
Requires order_has_fee to run first!
|
||||
:param order: Order or trade (one trade) dict
|
||||
:param fee: ccxt Fee dict - must contain cost / currency / rate
|
||||
:param symbol: Symbol of the order
|
||||
:param cost: Total cost of the order
|
||||
:param amount: Amount of the order
|
||||
:return: Tuple with cost, currency, rate of the given fee dict
|
||||
"""
|
||||
return (order['fee']['cost'],
|
||||
order['fee']['currency'],
|
||||
self.calculate_fee_rate(order))
|
||||
return (fee['cost'],
|
||||
fee['currency'],
|
||||
self.calculate_fee_rate(
|
||||
fee,
|
||||
symbol,
|
||||
cost,
|
||||
amount
|
||||
)
|
||||
)
|
||||
|
||||
# Historic data
|
||||
|
||||
|
@@ -32,7 +32,8 @@ class Gateio(Exchange):
|
||||
}
|
||||
|
||||
_ft_has_futures: Dict = {
|
||||
"needs_trading_fees": True
|
||||
"needs_trading_fees": True,
|
||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||
|
@@ -28,6 +28,7 @@ class Okx(Exchange):
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"tickers_have_quoteVolume": False,
|
||||
"fee_cost_in_contracts": True,
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||
|
@@ -67,7 +67,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
|
||||
init_db(self.config.get('db_url', None))
|
||||
init_db(self.config['db_url'])
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange)
|
||||
|
||||
@@ -634,7 +634,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
|
||||
entry_tag=enter_tag, side=trade_side):
|
||||
logger.info(f"User requested abortion of buying {pair}")
|
||||
logger.info(f"User denied entry for {pair}.")
|
||||
return False
|
||||
order = self.exchange.create_order(
|
||||
pair=pair,
|
||||
@@ -648,7 +648,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, side)
|
||||
order_id = order['id']
|
||||
order_status = order.get('status', None)
|
||||
order_status = order.get('status')
|
||||
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
|
||||
|
||||
# we assume the order is executed at the price requested
|
||||
@@ -814,7 +814,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||
min_stake=min_stake_amount, max_stake=min(max_stake_amount, stake_available),
|
||||
entry_tag=entry_tag, side=trade_side
|
||||
leverage=leverage, entry_tag=entry_tag, side=trade_side
|
||||
)
|
||||
|
||||
stake_amount = self.wallets.validate_stake_amount(
|
||||
@@ -969,6 +969,29 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
|
||||
return False
|
||||
|
||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
|
||||
"""
|
||||
Check and execute trade exit
|
||||
"""
|
||||
exits: List[ExitCheckTuple] = self.strategy.should_exit(
|
||||
trade,
|
||||
exit_rate,
|
||||
datetime.now(timezone.utc),
|
||||
enter=enter,
|
||||
exit_=exit_,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
for should_exit in exits:
|
||||
if should_exit.exit_flag:
|
||||
exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||
f'{f" Tag: {exit_tag1}" if exit_tag1 is not None else ""}')
|
||||
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag1)
|
||||
if exited:
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
|
||||
"""
|
||||
Abstracts creating stoploss orders from the logic.
|
||||
@@ -1120,28 +1143,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
|
||||
"""
|
||||
Check and execute trade exit
|
||||
"""
|
||||
exits: List[ExitCheckTuple] = self.strategy.should_exit(
|
||||
trade,
|
||||
exit_rate,
|
||||
datetime.now(timezone.utc),
|
||||
enter=enter,
|
||||
exit_=exit_,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
for should_exit in exits:
|
||||
if should_exit.exit_flag:
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||
f'{f" Tag: {exit_tag}" if exit_tag is not None else ""}')
|
||||
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||
if exited:
|
||||
return True
|
||||
return False
|
||||
|
||||
def manage_open_orders(self) -> None:
|
||||
"""
|
||||
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
|
||||
@@ -1474,7 +1475,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
time_in_force=time_in_force, exit_reason=exit_reason,
|
||||
sell_reason=exit_reason, # sellreason -> compatibility
|
||||
current_time=datetime.now(timezone.utc)):
|
||||
logger.info(f"User requested abortion of {trade.pair} exit.")
|
||||
logger.info(f"User denied exit for {trade.pair}.")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -1551,7 +1552,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
@@ -1672,7 +1673,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
# If a entry order was closed, force update on stoploss on exchange
|
||||
if order.get('side', None) == trade.entry_side:
|
||||
if order.get('side') == trade.entry_side:
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
# TODO: Margin will need to use interest_rate as well.
|
||||
# interest_rate = self.exchange.get_interest_rate()
|
||||
@@ -1761,7 +1762,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
|
||||
# use fee from order-dict if possible
|
||||
if self.exchange.order_has_fee(order):
|
||||
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
||||
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(
|
||||
order['fee'], order['symbol'], order['cost'], order_obj.safe_filled)
|
||||
logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
|
||||
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
||||
if fee_rate is None or fee_rate < 0.02:
|
||||
@@ -1799,7 +1801,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
for exectrade in trades:
|
||||
amount += exectrade['amount']
|
||||
if self.exchange.order_has_fee(exectrade):
|
||||
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade)
|
||||
# Prefer singular fee
|
||||
fees = [exectrade['fee']]
|
||||
else:
|
||||
fees = exectrade.get('fees', [])
|
||||
for fee in fees:
|
||||
|
||||
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(
|
||||
fee, exectrade['symbol'], exectrade['cost'], exectrade['amount']
|
||||
)
|
||||
fee_cost += fee_cost_
|
||||
if fee_rate_ is not None:
|
||||
fee_rate_array.append(fee_rate_)
|
||||
|
@@ -87,7 +87,7 @@ class Backtesting:
|
||||
self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
|
||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||
|
||||
if self.config.get('strategy_list', None):
|
||||
if self.config.get('strategy_list'):
|
||||
for strat in list(self.config['strategy_list']):
|
||||
stratconf = deepcopy(self.config)
|
||||
stratconf['strategy'] = strat
|
||||
@@ -189,6 +189,7 @@ class Backtesting:
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
|
||||
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):
|
||||
@@ -721,7 +722,7 @@ class Backtesting:
|
||||
pair=pair, current_time=current_time, current_rate=propose_rate,
|
||||
proposed_stake=stake_amount, min_stake=min_stake_amount,
|
||||
max_stake=min(stake_available, max_stake_amount),
|
||||
entry_tag=entry_tag, side=direction)
|
||||
leverage=leverage, entry_tag=entry_tag, side=direction)
|
||||
|
||||
stake_amount_val = self.wallets.validate_stake_amount(
|
||||
pair=pair,
|
||||
@@ -1140,8 +1141,6 @@ class Backtesting:
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
self._set_strategy(strat)
|
||||
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||
|
||||
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
# Must come from strategy config, as the strategy may modify this setting.
|
||||
|
@@ -455,7 +455,7 @@ class Hyperopt:
|
||||
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
||||
|
||||
def start(self) -> None:
|
||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state'))
|
||||
logger.info(f"Using optimizer random state: {self.random_state}")
|
||||
self.hyperopt_table_header = -1
|
||||
# Initialize spaces ...
|
||||
|
@@ -127,14 +127,14 @@ class HyperoptTools():
|
||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time'),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time'),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit'),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit'),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit'),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit'),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective'),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective'),
|
||||
}
|
||||
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
||||
# No file found.
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy import inspect, select, text, tuple_, update
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.persistence.trade_model import Order, Trade
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -251,31 +252,31 @@ def set_sqlite_to_wal(engine):
|
||||
|
||||
def fix_old_dry_orders(engine):
|
||||
with engine.begin() as connection:
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
update orders
|
||||
set ft_is_open = 0
|
||||
where ft_is_open = 1 and (ft_trade_id, order_id) not in (
|
||||
select id, stoploss_order_id from trades where stoploss_order_id is not null
|
||||
) and ft_order_side = 'stoploss'
|
||||
and order_id like 'dry_%'
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
update orders
|
||||
set ft_is_open = 0
|
||||
where ft_is_open = 1
|
||||
and (ft_trade_id, order_id) not in (
|
||||
select id, open_order_id from trades where open_order_id is not null
|
||||
) and ft_order_side != 'stoploss'
|
||||
and order_id like 'dry_%'
|
||||
"""
|
||||
)
|
||||
)
|
||||
stmt = update(Order).where(
|
||||
Order.ft_is_open.is_(True),
|
||||
tuple_(Order.ft_trade_id, Order.order_id).not_in(
|
||||
select(
|
||||
Trade.id, Trade.stoploss_order_id
|
||||
).where(Trade.stoploss_order_id.is_not(None))
|
||||
),
|
||||
Order.ft_order_side == 'stoploss',
|
||||
Order.order_id.like('dry%'),
|
||||
|
||||
).values(ft_is_open=False)
|
||||
connection.execute(stmt)
|
||||
|
||||
stmt = update(Order).where(
|
||||
Order.ft_is_open.is_(True),
|
||||
tuple_(Order.ft_trade_id, Order.order_id).not_in(
|
||||
select(
|
||||
Trade.id, Trade.open_order_id
|
||||
).where(Trade.open_order_id.is_not(None))
|
||||
),
|
||||
Order.ft_order_side != 'stoploss',
|
||||
Order.order_id.like('dry%')
|
||||
|
||||
).values(ft_is_open=False)
|
||||
connection.execute(stmt)
|
||||
|
||||
|
||||
def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
|
@@ -821,7 +821,7 @@ class LocalTrade():
|
||||
self.open_rate = total_stake / total_amount
|
||||
self.stake_amount = total_stake / (self.leverage or 1.0)
|
||||
self.amount = total_amount
|
||||
self.fee_open_cost = self.fee_open * self.stake_amount
|
||||
self.fee_open_cost = self.fee_open * total_stake
|
||||
self.recalc_open_trade_value()
|
||||
if self.stop_loss_pct is not None and self.open_rate is not None:
|
||||
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
|
||||
|
@@ -30,7 +30,7 @@ class AgeFilter(IPairList):
|
||||
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||
|
||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
||||
self._max_days_listed = pairlistconfig.get('max_days_listed')
|
||||
|
||||
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
||||
if self._min_days_listed < 1:
|
||||
|
@@ -21,7 +21,7 @@ class PerformanceFilter(IPairList):
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._minutes = pairlistconfig.get('minutes', 0)
|
||||
self._min_profit = pairlistconfig.get('min_profit', None)
|
||||
self._min_profit = pairlistconfig.get('min_profit')
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
|
@@ -27,7 +27,7 @@ class RangeStabilityFilter(IPairList):
|
||||
|
||||
self._days = pairlistconfig.get('lookback_days', 10)
|
||||
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
||||
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None)
|
||||
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change')
|
||||
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
||||
self._def_candletype = self._config['candle_type_def']
|
||||
|
||||
|
@@ -28,7 +28,7 @@ class PairListManager(LoggingMixin):
|
||||
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||
self._pairlist_handlers: List[IPairList] = []
|
||||
self._tickers_needed = False
|
||||
for pairlist_handler_config in self._config.get('pairlists', None):
|
||||
for pairlist_handler_config in self._config.get('pairlists', []):
|
||||
pairlist_handler = PairListResolver.load_pairlist(
|
||||
pairlist_handler_config['method'],
|
||||
exchange=exchange,
|
||||
|
@@ -282,7 +282,7 @@ def get_strategy(strategy: str, config=Depends(get_config)):
|
||||
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
|
||||
candletype: Optional[CandleType] = None, config=Depends(get_config)):
|
||||
|
||||
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
|
||||
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv'))
|
||||
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)
|
||||
|
||||
|
@@ -97,7 +97,7 @@ class RPC:
|
||||
"""
|
||||
self._freqtrade = freqtrade
|
||||
self._config: Dict[str, Any] = freqtrade.config
|
||||
if self._config.get('fiat_display_currency', None):
|
||||
if self._config.get('fiat_display_currency'):
|
||||
self._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
@staticmethod
|
||||
@@ -566,7 +566,7 @@ class RPC:
|
||||
else:
|
||||
try:
|
||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||
rate = tickers.get(pair, {}).get('last', None)
|
||||
rate = tickers.get(pair, {}).get('last')
|
||||
if rate:
|
||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||
rate = 1.0 / rate
|
||||
|
@@ -261,7 +261,7 @@ class Telegram(RPCHandler):
|
||||
)
|
||||
if msg.get('analyzed_candle'):
|
||||
message += f"*Analyzed Candle:* `{msg['analyzed_candle']}`\n"
|
||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
|
||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
|
||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
||||
message += f"*Leverage:* `{msg['leverage']}`\n"
|
||||
@@ -274,7 +274,7 @@ class Telegram(RPCHandler):
|
||||
|
||||
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
||||
|
||||
if msg.get('fiat_currency', None):
|
||||
if msg.get('fiat_currency'):
|
||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||
|
||||
message += ")`"
|
||||
@@ -290,7 +290,7 @@ class Telegram(RPCHandler):
|
||||
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
||||
msg['emoji'] = self._get_sell_emoji(msg)
|
||||
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
|
||||
if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0
|
||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
|
||||
else "")
|
||||
|
||||
# Check if all sell properties are available.
|
||||
|
@@ -45,21 +45,21 @@ class Webhook(RPCHandler):
|
||||
try:
|
||||
whconfig = self._config['webhook']
|
||||
if msg['type'] in [RPCMessageType.ENTRY]:
|
||||
valuedict = whconfig.get('webhookentry', None)
|
||||
valuedict = whconfig.get('webhookentry')
|
||||
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
||||
valuedict = whconfig.get('webhookentrycancel', None)
|
||||
valuedict = whconfig.get('webhookentrycancel')
|
||||
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
||||
valuedict = whconfig.get('webhookentryfill', None)
|
||||
valuedict = whconfig.get('webhookentryfill')
|
||||
elif msg['type'] == RPCMessageType.EXIT:
|
||||
valuedict = whconfig.get('webhookexit', None)
|
||||
valuedict = whconfig.get('webhookexit')
|
||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||
valuedict = whconfig.get('webhookexitfill', None)
|
||||
valuedict = whconfig.get('webhookexitfill')
|
||||
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
||||
valuedict = whconfig.get('webhookexitcancel', None)
|
||||
valuedict = whconfig.get('webhookexitcancel')
|
||||
elif msg['type'] in (RPCMessageType.STATUS,
|
||||
RPCMessageType.STARTUP,
|
||||
RPCMessageType.WARNING):
|
||||
valuedict = whconfig.get('webhookstatus', None)
|
||||
valuedict = whconfig.get('webhookstatus')
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||
if not valuedict:
|
||||
|
@@ -191,6 +191,7 @@ def detect_parameters(
|
||||
and attr.category is not None and attr.category != category):
|
||||
raise OperationalException(
|
||||
f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
|
||||
|
||||
if (category == attr.category or
|
||||
(attr_name.startswith(category + '_') and attr.category is None)):
|
||||
yield attr_name, attr
|
||||
|
@@ -442,7 +442,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
|
||||
@@ -452,6 +453,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param proposed_stake: A stake amount proposed by the bot.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param leverage: Leverage selected for this trade.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:return: A stake size, which is between min_stake and max_stake.
|
||||
|
@@ -79,9 +79,10 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
|
||||
"""
|
||||
return proposed_rate
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
|
||||
leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
"""
|
||||
Customize stake size for each new trade.
|
||||
|
||||
@@ -91,6 +92,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
|
||||
:param proposed_stake: A stake amount proposed by the bot.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param leverage: Leverage selected for this trade.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:return: A stake size, which is between min_stake and max_stake.
|
||||
|
@@ -131,9 +131,9 @@ class Wallets:
|
||||
if isinstance(balances[currency], dict):
|
||||
self._wallets[currency] = Wallet(
|
||||
currency,
|
||||
balances[currency].get('free', None),
|
||||
balances[currency].get('used', None),
|
||||
balances[currency].get('total', None)
|
||||
balances[currency].get('free'),
|
||||
balances[currency].get('used'),
|
||||
balances[currency].get('total')
|
||||
)
|
||||
# Remove currencies no longer in get_balances output
|
||||
for currency in deepcopy(self._wallets):
|
||||
|
Reference in New Issue
Block a user