Merge branch 'freqtrade:develop' into plot_hyperopt_stats
This commit is contained in:
@@ -75,7 +75,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||
"timerange", "timeframe", "no_trades"]
|
||||
|
||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||
"trade_source", "timeframe", "plot_auto_open"]
|
||||
"trade_source", "timeframe", "plot_auto_open", ]
|
||||
|
||||
ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version']
|
||||
|
||||
|
@@ -112,7 +112,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"ftx",
|
||||
"kucoin",
|
||||
"gateio",
|
||||
"okex",
|
||||
"okx",
|
||||
Separator(),
|
||||
"other",
|
||||
],
|
||||
@@ -140,7 +140,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okex')
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx')
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
|
@@ -182,11 +182,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
'--export-filename',
|
||||
help='Save backtest results to the file with this filename. '
|
||||
'Requires `--export` to be set as well. '
|
||||
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
||||
metavar='PATH',
|
||||
"--export-filename",
|
||||
"--backtest-filename",
|
||||
help="Use this filename for backtest results."
|
||||
"Requires `--export` to be set as well. "
|
||||
"Example: `--export-filename=user_data/backtest_results/backtest_today.json`",
|
||||
metavar="PATH",
|
||||
),
|
||||
"disableparamexport": Arg(
|
||||
'--disable-param-export',
|
||||
|
@@ -431,7 +431,6 @@ class Configuration:
|
||||
logstring='Using "{}" to store trades data.')
|
||||
|
||||
def _process_data_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
self._args_to_config(config, argname='new_pairs_days',
|
||||
logstring='Detected --new-pairs-days: {}')
|
||||
|
||||
|
@@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||
'CalmarHyperOptLoss',
|
||||
'MaxDrawDownHyperOptLoss']
|
||||
'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
@@ -456,6 +456,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
||||
'dry_run_wallet',
|
||||
'dataformat_ohlcv',
|
||||
'dataformat_trades',
|
||||
'unfilledtimeout',
|
||||
]
|
||||
|
||||
SCHEMA_MINIMAL_REQUIRED = [
|
||||
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
@@ -208,7 +208,7 @@ def _download_pair_history(pair: str, *,
|
||||
else:
|
||||
# Run cleaning again to ensure there were no duplicate candles
|
||||
# Especially between existing and new data.
|
||||
data = clean_ohlcv_dataframe(data.append(new_dataframe), timeframe, pair,
|
||||
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair,
|
||||
fill_missing=False, drop_incomplete=False)
|
||||
|
||||
logger.debug("New Start: %s",
|
||||
|
@@ -20,4 +20,4 @@ from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.okex import Okex
|
||||
from freqtrade.exchange.okx import Okx
|
||||
|
@@ -27,13 +27,15 @@ API_FETCH_ORDER_RETRY_COUNT = 5
|
||||
|
||||
BAD_EXCHANGES = {
|
||||
"bitmex": "Various reasons.",
|
||||
"phemex": "Does not provide history. ",
|
||||
"phemex": "Does not provide history.",
|
||||
"probit": "Requires additional, regular calls to `signIn()`.",
|
||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
||||
}
|
||||
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
'binanceus': 'binance',
|
||||
'binanceje': 'binance',
|
||||
'okex': 'okx',
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1587,7 +1587,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okex']
|
||||
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx']
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
|
@@ -7,8 +7,8 @@ from freqtrade.exchange import Exchange
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Okex(Exchange):
|
||||
"""Okex exchange class.
|
||||
class Okx(Exchange):
|
||||
"""Okx exchange class.
|
||||
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
"""
|
@@ -100,6 +100,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
self._exit_lock = Lock()
|
||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||
|
||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
"""
|
||||
Public method for users of this class (worker, etc.) to send notifications
|
||||
@@ -187,6 +189,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.enter_positions()
|
||||
|
||||
Trade.commit()
|
||||
self.last_process = datetime.now(timezone.utc)
|
||||
|
||||
def process_stopped(self) -> None:
|
||||
"""
|
||||
@@ -295,28 +298,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
|
||||
def handle_insufficient_funds(self, trade: Trade):
|
||||
"""
|
||||
Determine if we ever opened a sell order for this trade.
|
||||
If not, try update buy fees - otherwise "refind" the open order we obviously lost.
|
||||
"""
|
||||
sell_order = trade.select_order('sell', None)
|
||||
if sell_order:
|
||||
self.refind_lost_order(trade)
|
||||
else:
|
||||
self.reupdate_enter_order_fees(trade)
|
||||
|
||||
def reupdate_enter_order_fees(self, trade: Trade):
|
||||
"""
|
||||
Get buy order from database, and try to reupdate.
|
||||
Handles trades where the initial fee-update did not work.
|
||||
"""
|
||||
logger.info(f"Trying to reupdate buy fees for {trade}")
|
||||
order = trade.select_order('buy', False)
|
||||
if order:
|
||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
|
||||
def refind_lost_order(self, trade):
|
||||
"""
|
||||
Try refinding a lost trade.
|
||||
Only used when InsufficientFunds appears on sell orders (stoploss or sell).
|
||||
@@ -329,9 +310,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
if not order.ft_is_open:
|
||||
logger.debug(f"Order {order} is no longer open.")
|
||||
continue
|
||||
if order.ft_order_side == 'buy':
|
||||
# Skip buy side - this is handled by reupdate_buy_order_fees
|
||||
continue
|
||||
try:
|
||||
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
|
||||
order.ft_order_side == 'stoploss')
|
||||
@@ -343,6 +321,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open order
|
||||
trade.open_order_id = order.order_id
|
||||
elif order.ft_order_side == 'buy':
|
||||
if fo and fo['status'] == 'open':
|
||||
trade.open_order_id = order.order_id
|
||||
if fo:
|
||||
logger.info(f"Found {order} for trade {trade}.")
|
||||
self.update_trade_state(trade, order.order_id, fo,
|
||||
@@ -984,18 +965,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
||||
|
||||
order_obj = trade.select_order_by_order_id(trade.open_order_id)
|
||||
|
||||
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
or self.strategy.ft_check_timed_out(
|
||||
'buy', trade, order, datetime.now(timezone.utc))
|
||||
)):
|
||||
or (order_obj and self.strategy.ft_check_timed_out(
|
||||
'buy', trade, order_obj, datetime.now(timezone.utc))
|
||||
))):
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
or self.strategy.ft_check_timed_out(
|
||||
'sell', trade, order, datetime.now(timezone.utc)))
|
||||
):
|
||||
or (order_obj and self.strategy.ft_check_timed_out(
|
||||
'sell', trade, order_obj, datetime.now(timezone.utc))
|
||||
))):
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
canceled_count = trade.get_exit_order_count()
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
|
@@ -63,6 +63,8 @@ class Backtesting:
|
||||
LoggingMixin.show_output = False
|
||||
self.config = config
|
||||
self.results: Dict[str, Any] = {}
|
||||
self.trade_id_counter: int = 0
|
||||
self.order_id_counter: int = 0
|
||||
|
||||
config['dry_run'] = True
|
||||
self.run_ids: Dict[str, str] = {}
|
||||
@@ -231,6 +233,8 @@ class Backtesting:
|
||||
PairLocks.reset_locks()
|
||||
Trade.reset_trades()
|
||||
self.rejected_trades = 0
|
||||
self.timedout_entry_orders = 0
|
||||
self.timedout_exit_orders = 0
|
||||
self.dataprovider.clear_cache()
|
||||
if enable_protections:
|
||||
self._load_protections(self.strategy)
|
||||
@@ -275,6 +279,13 @@ class Backtesting:
|
||||
# Trim startup period from analyzed dataframe
|
||||
df_analyzed = processed[pair] = pair_data = trim_dataframe(
|
||||
df_analyzed, self.timerange, startup_candles=self.required_startup)
|
||||
# Update dataprovider cache
|
||||
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||
|
||||
# Create a copy of the dataframe before shifting, that way the buy signal/tag
|
||||
# remains on the correct candle for callbacks.
|
||||
df_analyzed = df_analyzed.copy()
|
||||
|
||||
# To avoid using data from future, we use buy/sell signals shifted
|
||||
# from the previous candle
|
||||
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
||||
@@ -282,9 +293,6 @@ class Backtesting:
|
||||
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
||||
df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1)
|
||||
|
||||
# Update dataprovider cache
|
||||
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||
|
||||
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
|
||||
|
||||
# Convert from Pandas to list for performance reasons
|
||||
@@ -349,7 +357,10 @@ class Backtesting:
|
||||
# use Open rate if open_rate > calculated sell rate
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
return close_rate
|
||||
# Use the maximum between close_rate and low as we
|
||||
# cannot sell outside of a candle.
|
||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
||||
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
||||
|
||||
else:
|
||||
# This should not be reached...
|
||||
@@ -372,10 +383,15 @@ class Backtesting:
|
||||
if stake_amount is not None and stake_amount > 0.0:
|
||||
pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade)
|
||||
if pos_trade is not None:
|
||||
self.wallets.update()
|
||||
return pos_trade
|
||||
|
||||
return trade
|
||||
|
||||
def _get_order_filled(self, rate: float, row: Tuple) -> bool:
|
||||
""" Rate is within candle, therefore filled"""
|
||||
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
||||
|
||||
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
||||
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
|
||||
@@ -401,18 +417,21 @@ class Backtesting:
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
# call the custom exit price,with default value as previous closerate
|
||||
current_profit = trade.calc_profit_ratio(closerate)
|
||||
order_type = self.strategy.order_types['sell']
|
||||
if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL):
|
||||
# Custom exit pricing only for sell-signals
|
||||
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||
default_retval=closerate)(
|
||||
pair=trade.pair, trade=trade,
|
||||
current_time=sell_row[DATE_IDX],
|
||||
proposed_rate=closerate, current_profit=current_profit)
|
||||
# Use the maximum between close_rate and low as we cannot sell outside of a candle.
|
||||
closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
||||
|
||||
if order_type == 'limit':
|
||||
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||
default_retval=closerate)(
|
||||
pair=trade.pair, trade=trade,
|
||||
current_time=sell_candle_time,
|
||||
proposed_rate=closerate, current_profit=current_profit)
|
||||
# We can't place orders lower than current low.
|
||||
# freqtrade does not support this in live, and the order would fill immediately
|
||||
closerate = max(closerate, sell_row[LOW_IDX])
|
||||
# Confirm trade exit:
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
||||
rate=closerate,
|
||||
@@ -432,7 +451,28 @@ class Backtesting:
|
||||
):
|
||||
trade.sell_reason = sell_row[EXIT_TAG_IDX]
|
||||
|
||||
trade.close(closerate, show_msg=False)
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=self.order_id_counter,
|
||||
ft_trade_id=trade.id,
|
||||
order_date=sell_candle_time,
|
||||
order_update_date=sell_candle_time,
|
||||
ft_is_open=True,
|
||||
ft_pair=trade.pair,
|
||||
order_id=str(self.order_id_counter),
|
||||
symbol=trade.pair,
|
||||
ft_order_side="sell",
|
||||
side="sell",
|
||||
order_type=order_type,
|
||||
status="open",
|
||||
price=closerate,
|
||||
average=closerate,
|
||||
amount=trade.amount,
|
||||
filled=0,
|
||||
remaining=trade.amount,
|
||||
cost=trade.amount * closerate,
|
||||
)
|
||||
trade.orders.append(order)
|
||||
return trade
|
||||
|
||||
return None
|
||||
@@ -471,13 +511,16 @@ class Backtesting:
|
||||
current_time = row[DATE_IDX].to_pydatetime()
|
||||
entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None
|
||||
# let's call the custom entry price, using the open price as default price
|
||||
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=row[OPEN_IDX])(
|
||||
pair=pair, current_time=current_time,
|
||||
proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate
|
||||
|
||||
# Move rate to within the candle's low/high rate
|
||||
propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX])
|
||||
order_type = self.strategy.order_types['buy']
|
||||
propose_rate = row[OPEN_IDX]
|
||||
if order_type == 'limit':
|
||||
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=row[OPEN_IDX])(
|
||||
pair=pair, current_time=current_time,
|
||||
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
|
||||
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
|
||||
# which freqtrade does not support in live.
|
||||
propose_rate = min(propose_rate, row[HIGH_IDX])
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
@@ -485,9 +528,9 @@ class Backtesting:
|
||||
pos_adjust = trade is not None
|
||||
if not pos_adjust:
|
||||
try:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
|
||||
except DependencyException:
|
||||
return trade
|
||||
return None
|
||||
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
@@ -502,8 +545,7 @@ class Backtesting:
|
||||
# If not pos adjust, trade is None
|
||||
return trade
|
||||
|
||||
order_type = self.strategy.order_types['buy']
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
# Confirm trade entry:
|
||||
if not pos_adjust:
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
@@ -513,15 +555,21 @@ class Backtesting:
|
||||
return None
|
||||
|
||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||
self.order_id_counter += 1
|
||||
amount = round(stake_amount / propose_rate, 8)
|
||||
if trade is None:
|
||||
# Enter trade
|
||||
self.trade_id_counter += 1
|
||||
trade = LocalTrade(
|
||||
id=self.trade_id_counter,
|
||||
open_order_id=self.order_id_counter,
|
||||
pair=pair,
|
||||
open_rate=propose_rate,
|
||||
open_rate_requested=propose_rate,
|
||||
open_date=current_time,
|
||||
stake_amount=stake_amount,
|
||||
amount=amount,
|
||||
amount_requested=amount,
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
@@ -529,28 +577,36 @@ class Backtesting:
|
||||
exchange='backtesting',
|
||||
orders=[]
|
||||
)
|
||||
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
|
||||
order = Order(
|
||||
ft_is_open=False,
|
||||
id=self.order_id_counter,
|
||||
ft_trade_id=trade.id,
|
||||
ft_is_open=True,
|
||||
ft_pair=trade.pair,
|
||||
order_id=str(self.order_id_counter),
|
||||
symbol=trade.pair,
|
||||
ft_order_side="buy",
|
||||
side="buy",
|
||||
order_type="market",
|
||||
status="closed",
|
||||
order_type=order_type,
|
||||
status="open",
|
||||
order_date=current_time,
|
||||
order_filled_date=current_time,
|
||||
order_update_date=current_time,
|
||||
price=propose_rate,
|
||||
average=propose_rate,
|
||||
amount=amount,
|
||||
filled=amount,
|
||||
cost=stake_amount + trade.fee_open
|
||||
filled=0,
|
||||
remaining=amount,
|
||||
cost=stake_amount + trade.fee_open,
|
||||
)
|
||||
if pos_adjust and self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_time)
|
||||
else:
|
||||
trade.open_order_id = str(self.order_id_counter)
|
||||
trade.orders.append(order)
|
||||
if pos_adjust:
|
||||
trade.recalc_trade_from_orders()
|
||||
trade.recalc_trade_from_orders()
|
||||
|
||||
return trade
|
||||
|
||||
@@ -563,6 +619,9 @@ class Backtesting:
|
||||
for pair in open_trades.keys():
|
||||
if len(open_trades[pair]) > 0:
|
||||
for trade in open_trades[pair]:
|
||||
if trade.open_order_id and trade.nr_of_successful_buys == 0:
|
||||
# Ignore trade if buy-order did not fill yet
|
||||
continue
|
||||
sell_row = data[pair][-1]
|
||||
|
||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||
@@ -583,6 +642,51 @@ class Backtesting:
|
||||
self.rejected_trades += 1
|
||||
return False
|
||||
|
||||
def run_protections(self, enable_protections, pair: str, current_time: datetime):
|
||||
if enable_protections:
|
||||
self.protections.stop_per_pair(pair, current_time)
|
||||
self.protections.global_stop(current_time)
|
||||
|
||||
def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
|
||||
"""
|
||||
Check if an order has been canceled.
|
||||
Returns True if the trade should be Deleted (initial order was canceled).
|
||||
"""
|
||||
for order in [o for o in trade.orders if o.ft_is_open]:
|
||||
|
||||
timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time)
|
||||
if timedout:
|
||||
if order.side == 'buy':
|
||||
self.timedout_entry_orders += 1
|
||||
if trade.nr_of_successful_buys == 0:
|
||||
# Remove trade due to buy timeout expiration.
|
||||
return True
|
||||
else:
|
||||
# Close additional buy order
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
if order.side == 'sell':
|
||||
self.timedout_exit_orders += 1
|
||||
# Close sell order and retry selling on next signal.
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
|
||||
return False
|
||||
|
||||
def validate_row(
|
||||
self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]:
|
||||
try:
|
||||
# Row is treated as "current incomplete candle".
|
||||
# Buy / sell signals are shifted by 1 to compensate for this.
|
||||
row = data[pair][row_index]
|
||||
except IndexError:
|
||||
# missing Data for one pair at the end.
|
||||
# Warnings for this are shown during data loading
|
||||
return None
|
||||
|
||||
# Waits until the time-counter reaches the start of the data for this pair.
|
||||
if row[DATE_IDX] > current_time:
|
||||
return None
|
||||
return row
|
||||
|
||||
def backtest(self, processed: Dict,
|
||||
start_date: datetime, end_date: datetime,
|
||||
max_open_trades: int = 0, position_stacking: bool = False,
|
||||
@@ -605,14 +709,15 @@ class Backtesting:
|
||||
"""
|
||||
trades: List[LocalTrade] = []
|
||||
self.prepare_backtest(enable_protections)
|
||||
|
||||
# Ensure wallets are uptodate (important for --strategy-list)
|
||||
self.wallets.update()
|
||||
# Use dict of lists with data for performance
|
||||
# (looping lists is a lot faster than pandas DataFrames)
|
||||
data: Dict = self._get_ohlcv_as_lists(processed)
|
||||
|
||||
# Indexes per pair, so some pairs are allowed to have a missing start.
|
||||
indexes: Dict = defaultdict(int)
|
||||
tmp = start_date + timedelta(minutes=self.timeframe_min)
|
||||
current_time = start_date + timedelta(minutes=self.timeframe_min)
|
||||
|
||||
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
|
||||
open_trade_count = 0
|
||||
@@ -621,35 +726,27 @@ class Backtesting:
|
||||
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
|
||||
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while tmp <= end_date:
|
||||
while current_time <= end_date:
|
||||
open_trade_count_start = open_trade_count
|
||||
self.check_abort()
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
try:
|
||||
# Row is treated as "current incomplete candle".
|
||||
# Buy / sell signals are shifted by 1 to compensate for this.
|
||||
row = data[pair][row_index]
|
||||
except IndexError:
|
||||
# missing Data for one pair at the end.
|
||||
# Warnings for this are shown during data loading
|
||||
continue
|
||||
|
||||
# Waits until the time-counter reaches the start of the data for this pair.
|
||||
if row[DATE_IDX] > tmp:
|
||||
row = self.validate_row(data, pair, row_index, current_time)
|
||||
if not row:
|
||||
continue
|
||||
|
||||
row_index += 1
|
||||
indexes[pair] = row_index
|
||||
self.dataprovider._set_dataframe_max_index(row_index)
|
||||
|
||||
# 1. Process buys.
|
||||
# without positionstacking, we can only have one open trade per pair.
|
||||
# max_open_trades must be respected
|
||||
# don't open on the last row
|
||||
if (
|
||||
(position_stacking or len(open_trades[pair]) == 0)
|
||||
and self.trade_slot_available(max_open_trades, open_trade_count_start)
|
||||
and tmp != end_date
|
||||
and current_time != end_date
|
||||
and row[BUY_IDX] == 1
|
||||
and row[SELL_IDX] != 1
|
||||
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])
|
||||
@@ -657,32 +754,51 @@ class Backtesting:
|
||||
trade = self._enter_trade(pair, row)
|
||||
if trade:
|
||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||
# This emulates previous behaviour - not sure if this is correct
|
||||
# This emulates previous behavior - not sure if this is correct
|
||||
# Prevents buying if the trade-slot was freed in this candle
|
||||
open_trade_count_start += 1
|
||||
open_trade_count += 1
|
||||
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
|
||||
open_trades[pair].append(trade)
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
for trade in list(open_trades[pair]):
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
# Sell occurred
|
||||
if trade_entry:
|
||||
# 2. Process buy orders.
|
||||
order = trade.select_order('buy', is_open=True)
|
||||
if order and self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_time)
|
||||
trade.open_order_id = None
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
self.wallets.update()
|
||||
|
||||
# 3. Create sell orders (if any)
|
||||
if not trade.open_order_id:
|
||||
self._get_sell_trade_entry(trade, row) # Place sell order if necessary
|
||||
|
||||
# 4. Process sell orders.
|
||||
order = trade.select_order('sell', is_open=True)
|
||||
if order and self._get_order_filled(order.price, row):
|
||||
trade.open_order_id = None
|
||||
trade.close_date = current_time
|
||||
trade.close(order.price, show_msg=False)
|
||||
|
||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
||||
open_trade_count -= 1
|
||||
open_trades[pair].remove(trade)
|
||||
|
||||
LocalTrade.close_bt_trade(trade)
|
||||
trades.append(trade_entry)
|
||||
if enable_protections:
|
||||
self.protections.stop_per_pair(pair, row[DATE_IDX])
|
||||
self.protections.global_stop(tmp)
|
||||
trades.append(trade)
|
||||
self.wallets.update()
|
||||
self.run_protections(enable_protections, pair, current_time)
|
||||
|
||||
# 5. Cancel expired buy/sell orders.
|
||||
if self.check_order_cancel(trade, current_time):
|
||||
# Close trade due to buy timeout expiration.
|
||||
open_trade_count -= 1
|
||||
open_trades[pair].remove(trade)
|
||||
self.wallets.update()
|
||||
|
||||
# Move time one configured time_interval ahead.
|
||||
self.progress.increment()
|
||||
tmp += timedelta(minutes=self.timeframe_min)
|
||||
current_time += timedelta(minutes=self.timeframe_min)
|
||||
|
||||
trades += self.handle_left_open(open_trades, data=data)
|
||||
self.wallets.update()
|
||||
@@ -693,6 +809,8 @@ class Backtesting:
|
||||
'config': self.strategy.config,
|
||||
'locks': PairLocks.get_all_locks(),
|
||||
'rejected_signals': self.rejected_trades,
|
||||
'timedout_entry_orders': self.timedout_entry_orders,
|
||||
'timedout_exit_orders': self.timedout_exit_orders,
|
||||
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
||||
}
|
||||
|
||||
|
30
freqtrade/optimize/hyperopt_loss_profit_drawdown.py
Normal file
30
freqtrade/optimize/hyperopt_loss_profit_drawdown.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
ProfitDrawDownHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class based on Profit &
|
||||
Drawdown objective which can be used for Hyperoptimization.
|
||||
|
||||
Possible to change `DRAWDOWN_MULT` to penalize drawdown objective for
|
||||
individual needs.
|
||||
"""
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.btanalysis import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
# higher numbers penalize drawdowns more severely
|
||||
DRAWDOWN_MULT = 0.075
|
||||
|
||||
|
||||
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int, *args, **kwargs) -> float:
|
||||
total_profit = results["profit_abs"].sum()
|
||||
|
||||
try:
|
||||
max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs")[5]
|
||||
except ValueError:
|
||||
max_drawdown_abs = 0
|
||||
|
||||
return -1 * (total_profit * (1 - max_drawdown_abs * DRAWDOWN_MULT))
|
@@ -436,6 +436,8 @@ def generate_strategy_stats(pairlist: List[str],
|
||||
'dry_run_wallet': starting_balance,
|
||||
'final_balance': content['final_balance'],
|
||||
'rejected_signals': content['rejected_signals'],
|
||||
'timedout_entry_orders': content['timedout_entry_orders'],
|
||||
'timedout_exit_orders': content['timedout_exit_orders'],
|
||||
'max_open_trades': max_open_trades,
|
||||
'max_open_trades_setting': (config['max_open_trades']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
@@ -726,6 +728,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
|
||||
('Entry/Exit Timeouts',
|
||||
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
|
||||
f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
|
||||
('', ''), # Empty line to improve readability
|
||||
|
||||
('Min balance', round_coin_value(strat_results['csum_min'],
|
||||
|
@@ -28,7 +28,36 @@ def get_backup_name(tabs, backup_prefix: str):
|
||||
return table_back_name
|
||||
|
||||
|
||||
def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, cols: List):
|
||||
def get_last_sequence_ids(engine, trade_back_name, order_back_name):
|
||||
order_id: int = None
|
||||
trade_id: int = None
|
||||
|
||||
if engine.name == 'postgresql':
|
||||
with engine.begin() as connection:
|
||||
trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0]
|
||||
order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0]
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(
|
||||
f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak"))
|
||||
connection.execute(text(
|
||||
f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak"))
|
||||
return order_id, trade_id
|
||||
|
||||
|
||||
def set_sequence_ids(engine, order_id, trade_id):
|
||||
|
||||
if engine.name == 'postgresql':
|
||||
with engine.begin() as connection:
|
||||
if order_id:
|
||||
connection.execute(text(f"ALTER SEQUENCE orders_id_seq RESTART WITH {order_id}"))
|
||||
if trade_id:
|
||||
connection.execute(text(f"ALTER SEQUENCE trades_id_seq RESTART WITH {trade_id}"))
|
||||
|
||||
|
||||
def migrate_trades_and_orders_table(
|
||||
decl_base, inspector, engine,
|
||||
trade_back_name: str, cols: List,
|
||||
order_back_name: str):
|
||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
|
||||
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
|
||||
@@ -64,11 +93,20 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
||||
|
||||
# Schema migration necessary
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(f"alter table trades rename to {table_back_name}"))
|
||||
connection.execute(text(f"alter table trades rename to {trade_back_name}"))
|
||||
|
||||
with engine.begin() as connection:
|
||||
# drop indexes on backup table in new session
|
||||
for index in inspector.get_indexes(table_back_name):
|
||||
connection.execute(text(f"drop index {index['name']}"))
|
||||
for index in inspector.get_indexes(trade_back_name):
|
||||
if engine.name == 'mysql':
|
||||
connection.execute(text(f"drop index {index['name']} on {trade_back_name}"))
|
||||
else:
|
||||
connection.execute(text(f"drop index {index['name']}"))
|
||||
|
||||
order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name)
|
||||
|
||||
drop_orders_table(engine, order_back_name)
|
||||
|
||||
# let SQLAlchemy create the schema as required
|
||||
decl_base.metadata.create_all(engine)
|
||||
|
||||
@@ -100,9 +138,12 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
||||
{sell_order_status} sell_order_status,
|
||||
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
|
||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
|
||||
from {table_back_name}
|
||||
from {trade_back_name}
|
||||
"""))
|
||||
|
||||
migrate_orders_table(engine, order_back_name, cols)
|
||||
set_sequence_ids(engine, order_id, trade_id)
|
||||
|
||||
|
||||
def migrate_open_orders_to_trades(engine):
|
||||
with engine.begin() as connection:
|
||||
@@ -121,19 +162,18 @@ def migrate_open_orders_to_trades(engine):
|
||||
"""))
|
||||
|
||||
|
||||
def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List):
|
||||
# Schema migration necessary
|
||||
def drop_orders_table(engine, table_back_name: str):
|
||||
# Drop and recreate orders table as backup
|
||||
# This drops foreign keys, too.
|
||||
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(f"alter table orders rename to {table_back_name}"))
|
||||
connection.execute(text(f"create table {table_back_name} as select * from orders"))
|
||||
connection.execute(text("drop table orders"))
|
||||
|
||||
with engine.begin() as connection:
|
||||
# drop indexes on backup table in new session
|
||||
for index in inspector.get_indexes(table_back_name):
|
||||
connection.execute(text(f"drop index {index['name']}"))
|
||||
|
||||
def migrate_orders_table(engine, table_back_name: str, cols: List):
|
||||
|
||||
# let SQLAlchemy create the schema as required
|
||||
decl_base.metadata.create_all(engine)
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text(f"""
|
||||
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||
@@ -155,11 +195,16 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
cols = inspector.get_columns('trades')
|
||||
tabs = get_table_names_for_table(inspector, 'trades')
|
||||
table_back_name = get_backup_name(tabs, 'trades_bak')
|
||||
order_tabs = get_table_names_for_table(inspector, 'orders')
|
||||
order_table_bak_name = get_backup_name(order_tabs, 'orders_bak')
|
||||
|
||||
# Check for latest column
|
||||
# Check if migration necessary
|
||||
# Migrates both trades and orders table!
|
||||
if not has_column(cols, 'buy_tag'):
|
||||
logger.info(f'Running database migration for trades - backup: {table_back_name}')
|
||||
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
||||
logger.info(f"Running database migration for trades - "
|
||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||
migrate_trades_and_orders_table(
|
||||
decl_base, inspector, engine, table_back_name, cols, order_table_bak_name)
|
||||
# Reread columns - the above recreated the table!
|
||||
inspector = inspect(engine)
|
||||
cols = inspector.get_columns('trades')
|
||||
@@ -167,12 +212,3 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
if 'orders' not in previous_tables and 'trades' in previous_tables:
|
||||
logger.info('Moving open orders to Orders table.')
|
||||
migrate_open_orders_to_trades(engine)
|
||||
else:
|
||||
cols_order = inspector.get_columns('orders')
|
||||
|
||||
if not has_column(cols_order, 'average'):
|
||||
tabs = get_table_names_for_table(inspector, 'orders')
|
||||
# Empty for now - as there is only one iteration of the orders table so far.
|
||||
table_back_name = get_backup_name(tabs, 'orders_bak')
|
||||
|
||||
migrate_orders_table(decl_base, inspector, engine, table_back_name, cols)
|
||||
|
@@ -132,6 +132,10 @@ class Order(_DECL_BASE):
|
||||
order_filled_date = Column(DateTime, nullable=True)
|
||||
order_update_date = Column(DateTime, nullable=True)
|
||||
|
||||
@property
|
||||
def order_date_utc(self):
|
||||
return self.order_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
|
||||
@@ -165,6 +169,35 @@ class Order(_DECL_BASE):
|
||||
self.order_filled_date = datetime.now(timezone.utc)
|
||||
self.order_update_date = datetime.now(timezone.utc)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'amount': self.amount,
|
||||
'average': round(self.average, 8) if self.average else 0,
|
||||
'cost': self.cost if self.cost else 0,
|
||||
'filled': self.filled,
|
||||
'ft_order_side': self.ft_order_side,
|
||||
'is_open': self.ft_is_open,
|
||||
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_date else None,
|
||||
'order_timestamp': int(self.order_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
|
||||
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.order_filled_date else None,
|
||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||
'order_type': self.order_type,
|
||||
'pair': self.ft_pair,
|
||||
'price': self.price,
|
||||
'remaining': self.remaining,
|
||||
'status': self.status,
|
||||
}
|
||||
|
||||
def close_bt_order(self, close_date: datetime):
|
||||
self.order_filled_date = close_date
|
||||
self.filled = self.amount
|
||||
self.status = 'closed'
|
||||
self.ft_is_open = False
|
||||
|
||||
@staticmethod
|
||||
def update_orders(orders: List['Order'], order: Dict[str, Any]):
|
||||
"""
|
||||
@@ -282,6 +315,16 @@ class LocalTrade():
|
||||
return self.close_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_orders()
|
||||
filled_entries = []
|
||||
filled_exits = []
|
||||
if len(filled_orders) > 0:
|
||||
for order in filled_orders:
|
||||
if order.ft_order_side == 'buy':
|
||||
filled_entries.append(order.to_json())
|
||||
if order.ft_order_side == 'sell':
|
||||
filled_exits.append(order.to_json())
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
'pair': self.pair,
|
||||
@@ -345,6 +388,8 @@ class LocalTrade():
|
||||
'max_rate': self.max_rate,
|
||||
|
||||
'open_order_id': self.open_order_id,
|
||||
'filled_entry_orders': filled_entries,
|
||||
'filled_exit_orders': filled_exits,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -600,14 +645,27 @@ class LocalTrade():
|
||||
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)
|
||||
|
||||
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
||||
def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
|
||||
"""
|
||||
Finds order object by Order id.
|
||||
:param order_id: Exchange order id
|
||||
"""
|
||||
for o in self.orders:
|
||||
if o.order_id == order_id:
|
||||
return o
|
||||
return None
|
||||
|
||||
def select_order(
|
||||
self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]:
|
||||
"""
|
||||
Finds latest order for this orderside and status
|
||||
:param order_side: Side of the order (either 'buy' or 'sell')
|
||||
:param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
|
||||
:param is_open: Only search for open orders?
|
||||
:return: latest Order object if it exists, else None
|
||||
"""
|
||||
orders = [o for o in self.orders if o.side == order_side]
|
||||
orders = self.orders
|
||||
if order_side:
|
||||
orders = [o for o in self.orders if o.ft_order_side == order_side]
|
||||
if is_open is not None:
|
||||
orders = [o for o in orders if o.ft_is_open == is_open]
|
||||
if len(orders) > 0:
|
||||
@@ -615,14 +673,14 @@ class LocalTrade():
|
||||
else:
|
||||
return None
|
||||
|
||||
def select_filled_orders(self, order_side: str) -> List['Order']:
|
||||
def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']:
|
||||
"""
|
||||
Finds filled orders for this orderside.
|
||||
:param order_side: Side of the order (either 'buy' or 'sell')
|
||||
:param order_side: Side of the order (either 'buy', 'sell', or None)
|
||||
:return: array of Order objects
|
||||
"""
|
||||
return [o for o in self.orders if o.ft_order_side == order_side and
|
||||
o.ft_is_open is False and
|
||||
return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
|
||||
and o.ft_is_open is False and
|
||||
(o.filled or 0) > 0 and
|
||||
o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
|
||||
|
@@ -61,8 +61,8 @@ def init_plotscript(config, markets: List, startup_candles: int = 0):
|
||||
startup_candles, min_date)
|
||||
|
||||
no_trades = False
|
||||
filename = config.get('exportfilename')
|
||||
if config.get('no_trades', False):
|
||||
filename = config.get("exportfilename")
|
||||
if config.get("no_trades", False):
|
||||
no_trades = True
|
||||
elif config['trade_source'] == 'file':
|
||||
if not filename.is_dir() and not filename.is_file():
|
||||
|
@@ -60,6 +60,7 @@ class PerformanceFilter(IPairList):
|
||||
|
||||
# Get pairlist from performance dataframe values
|
||||
list_df = pd.DataFrame({'pair': pairlist})
|
||||
list_df['prior_idx'] = list_df.index
|
||||
|
||||
# Set initial value for pairs with no trades to 0
|
||||
# Sort the list using:
|
||||
@@ -67,7 +68,7 @@ class PerformanceFilter(IPairList):
|
||||
# - then count (low to high, so as to favor same performance with fewer trades)
|
||||
# - then pair name alphametically
|
||||
sorted_df = list_df.merge(performance, on='pair', how='left')\
|
||||
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
|
||||
.fillna(0).sort_values(by=['count', 'prior_idx'], ascending=True)\
|
||||
.sort_values(by=['profit_ratio'], ascending=False)
|
||||
if self._min_profit is not None:
|
||||
removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit]
|
||||
|
@@ -32,6 +32,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||
for setting in settings.keys():
|
||||
if settings[setting] is not None:
|
||||
btconfig[setting] = settings[setting]
|
||||
try:
|
||||
btconfig['stake_amount'] = float(btconfig['stake_amount'])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Force dry-run for backtesting
|
||||
btconfig['dry_run'] = True
|
||||
@@ -57,8 +61,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||
):
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
ApiServer._bt = Backtesting(btconfig)
|
||||
if ApiServer._bt.timeframe_detail:
|
||||
ApiServer._bt.load_bt_data_detail()
|
||||
ApiServer._bt.load_bt_data_detail()
|
||||
else:
|
||||
ApiServer._bt.config = btconfig
|
||||
ApiServer._bt.init_backtest()
|
||||
|
@@ -109,7 +109,7 @@ class SellReason(BaseModel):
|
||||
|
||||
class Stats(BaseModel):
|
||||
sell_reasons: Dict[str, SellReason]
|
||||
durations: Dict[str, Union[str, float]]
|
||||
durations: Dict[str, Optional[float]]
|
||||
|
||||
|
||||
class DailyRecord(BaseModel):
|
||||
@@ -149,7 +149,7 @@ class ShowConfig(BaseModel):
|
||||
api_version: float
|
||||
dry_run: bool
|
||||
stake_currency: str
|
||||
stake_amount: Union[float, str]
|
||||
stake_amount: str
|
||||
available_capital: Optional[float]
|
||||
stake_currency_decimals: int
|
||||
max_open_trades: int
|
||||
@@ -280,6 +280,7 @@ class ForceBuyPayload(BaseModel):
|
||||
price: Optional[float]
|
||||
ordertype: Optional[OrderTypeValues]
|
||||
stakeamount: Optional[float]
|
||||
entry_tag: Optional[str]
|
||||
|
||||
|
||||
class ForceSellPayload(BaseModel):
|
||||
@@ -365,7 +366,7 @@ class BacktestRequest(BaseModel):
|
||||
timeframe_detail: Optional[str]
|
||||
timerange: Optional[str]
|
||||
max_open_trades: Optional[int]
|
||||
stake_amount: Optional[Union[float, str]]
|
||||
stake_amount: Optional[str]
|
||||
enable_protections: bool
|
||||
dry_run_wallet: Optional[float]
|
||||
|
||||
@@ -384,3 +385,8 @@ class BacktestResponse(BaseModel):
|
||||
class SysInfo(BaseModel):
|
||||
cpu_pct: List[float]
|
||||
ram_pct: float
|
||||
|
||||
|
||||
class Health(BaseModel):
|
||||
last_process: datetime
|
||||
last_process_ts: int
|
||||
|
@@ -14,12 +14,12 @@ from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
|
||||
BlacklistResponse, Count, Daily,
|
||||
DeleteLockRequest, DeleteTrade, ForceBuyPayload,
|
||||
ForceBuyResponse, ForceSellPayload, Locks, Logs,
|
||||
OpenTradeSchema, PairHistory, PerformanceEntry,
|
||||
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
|
||||
Stats, StatusMsg, StrategyListResponse,
|
||||
StrategyResponse, SysInfo, Version,
|
||||
WhitelistResponse)
|
||||
ForceBuyResponse, ForceSellPayload, Health, Locks,
|
||||
Logs, OpenTradeSchema, PairHistory,
|
||||
PerformanceEntry, Ping, PlotConfig, Profit,
|
||||
ResultMsg, ShowConfig, Stats, StatusMsg,
|
||||
StrategyListResponse, StrategyResponse, SysInfo,
|
||||
Version, WhitelistResponse)
|
||||
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
|
||||
@@ -136,8 +136,9 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
stake_amount = payload.stakeamount if payload.stakeamount else None
|
||||
entry_tag = payload.entry_tag if payload.entry_tag else None
|
||||
|
||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount)
|
||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag)
|
||||
|
||||
if trade:
|
||||
return ForceBuyResponse.parse_obj(trade.to_json())
|
||||
@@ -291,3 +292,8 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
|
||||
@router.get('/sysinfo', response_model=SysInfo, tags=['info'])
|
||||
def sysinfo():
|
||||
return RPC._rpc_sysinfo()
|
||||
|
||||
|
||||
@router.get('/health', response_model=Health, tags=['info'])
|
||||
def health(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._health()
|
||||
|
@@ -17,6 +17,15 @@ from freqtrade.constants import SUPPORTED_FIAT
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Manually map symbol to ID for some common coins
|
||||
# with duplicate coingecko entries
|
||||
coingecko_mapping = {
|
||||
'eth': 'ethereum',
|
||||
'bnb': 'binancecoin',
|
||||
'sol': 'solana',
|
||||
}
|
||||
|
||||
|
||||
class CryptoToFiatConverter:
|
||||
"""
|
||||
Main class to initiate Crypto to FIAT.
|
||||
@@ -77,8 +86,9 @@ class CryptoToFiatConverter:
|
||||
else:
|
||||
return None
|
||||
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
|
||||
if crypto_symbol == 'eth':
|
||||
found = [x for x in self._coinlistings if x['id'] == 'ethereum']
|
||||
|
||||
if crypto_symbol in coingecko_mapping.keys():
|
||||
found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]]
|
||||
|
||||
if len(found) == 1:
|
||||
return found[0]['id']
|
||||
|
@@ -10,8 +10,9 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
import arrow
|
||||
import psutil
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from dateutil.tz import tzlocal
|
||||
from numpy import NAN, inf, int64, mean
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, NaT
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
@@ -111,7 +112,7 @@ class RPC:
|
||||
'dry_run': config['dry_run'],
|
||||
'stake_currency': config['stake_currency'],
|
||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||
'stake_amount': config['stake_amount'],
|
||||
'stake_amount': str(config['stake_amount']),
|
||||
'available_capital': config.get('available_capital'),
|
||||
'max_open_trades': (config['max_open_trades']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
@@ -263,7 +264,7 @@ class RPC:
|
||||
profitcol += " (" + fiat_display_currency + ")"
|
||||
|
||||
if self._config.get('position_adjustment_enable', False):
|
||||
columns = ['ID', 'Pair', 'Since', profitcol, '# Buys']
|
||||
columns = ['ID', 'Pair', 'Since', profitcol, '# Entries']
|
||||
else:
|
||||
columns = ['ID', 'Pair', 'Since', profitcol]
|
||||
return trades_list, columns, fiat_profit_sum
|
||||
@@ -439,9 +440,9 @@ class RPC:
|
||||
trade_dur = (trade.close_date - trade.open_date).total_seconds()
|
||||
dur[trade_win_loss(trade)].append(trade_dur)
|
||||
|
||||
wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A'
|
||||
draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A'
|
||||
losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A'
|
||||
wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else None
|
||||
draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else None
|
||||
losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None
|
||||
|
||||
durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
|
||||
return {'sell_reasons': sell_reasons, 'durations': durations}
|
||||
@@ -716,7 +717,8 @@ class RPC:
|
||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||
|
||||
def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
|
||||
stake_amount: Optional[float] = None) -> Optional[Trade]:
|
||||
stake_amount: Optional[float] = None,
|
||||
buy_tag: Optional[str] = None) -> Optional[Trade]:
|
||||
"""
|
||||
Handler for forcebuy <asset> <price>
|
||||
Buys a pair trade at the given or current price
|
||||
@@ -750,7 +752,7 @@ class RPC:
|
||||
order_type = self._freqtrade.strategy.order_types.get(
|
||||
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
||||
ordertype=order_type, trade=trade):
|
||||
ordertype=order_type, trade=trade, buy_tag=buy_tag):
|
||||
Trade.commit()
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
return trade
|
||||
@@ -962,8 +964,16 @@ class RPC:
|
||||
sell_mask = (dataframe['sell'] == 1)
|
||||
sell_signals = int(sell_mask.sum())
|
||||
dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
|
||||
dataframe = dataframe.replace([inf, -inf], NAN)
|
||||
dataframe = dataframe.replace({NAN: None})
|
||||
|
||||
# band-aid until this is fixed:
|
||||
# https://github.com/pandas-dev/pandas/issues/45836
|
||||
datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]']
|
||||
date_columns = dataframe.select_dtypes(include=datetime_types)
|
||||
for date_column in date_columns:
|
||||
# replace NaT with `None`
|
||||
dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None})
|
||||
|
||||
dataframe = dataframe.replace({inf: None, -inf: None, NAN: None})
|
||||
|
||||
res = {
|
||||
'pair': pair,
|
||||
@@ -1038,3 +1048,11 @@ class RPC:
|
||||
"cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
|
||||
"ram_pct": psutil.virtual_memory().percent
|
||||
}
|
||||
|
||||
def _health(self) -> Dict[str, Union[str, int]]:
|
||||
last_p = self._freqtrade.last_process
|
||||
return {
|
||||
'last_process': str(last_p),
|
||||
'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT),
|
||||
'last_process_ts': int(last_p.timestamp()),
|
||||
}
|
||||
|
@@ -113,7 +113,7 @@ class Telegram(RPCHandler):
|
||||
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||
r'/forcebuy$', r'/edge$', r'/help$', r'/version$']
|
||||
r'/forcebuy$', r'/edge$', r'/health$', r'/help$', r'/version$']
|
||||
# Create keys for generation
|
||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||
|
||||
@@ -173,6 +173,7 @@ class Telegram(RPCHandler):
|
||||
CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete),
|
||||
CommandHandler('logs', self._logs),
|
||||
CommandHandler('edge', self._edge),
|
||||
CommandHandler('health', self._health),
|
||||
CommandHandler('help', self._help),
|
||||
CommandHandler('version', self._version),
|
||||
]
|
||||
@@ -369,6 +370,48 @@ class Telegram(RPCHandler):
|
||||
else:
|
||||
return "\N{CROSS MARK}"
|
||||
|
||||
def _prepare_entry_details(self, filled_orders, base_currency, is_open):
|
||||
"""
|
||||
Prepare details of trade with entry adjustment enabled
|
||||
"""
|
||||
lines = []
|
||||
for x, order in enumerate(filled_orders):
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["amount"]
|
||||
cur_entry_average = order["average"]
|
||||
lines.append(" ")
|
||||
if x == 0:
|
||||
lines.append("*Entry #{}:*".format(x+1))
|
||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
||||
.format(cur_entry_amount, order["cost"], base_currency))
|
||||
lines.append("*Average Entry Price:* {}".format(cur_entry_average))
|
||||
else:
|
||||
sumA = 0
|
||||
sumB = 0
|
||||
for y in range(x):
|
||||
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"])
|
||||
sumB += filled_orders[y]["amount"]
|
||||
prev_avg_price = sumA/sumB
|
||||
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"])
|
||||
/ filled_orders[0]["average"])
|
||||
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price
|
||||
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
|
||||
days = dur_entry.days
|
||||
hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry))
|
||||
if is_open:
|
||||
lines.append("({})".format(cur_entry_datetime
|
||||
.humanize(granularity=["day", "hour", "minute"])))
|
||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
||||
.format(cur_entry_amount, order["cost"], base_currency))
|
||||
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)"
|
||||
.format(cur_entry_average, price_to_1st_entry))
|
||||
lines.append("*Order filled at:* {}".format(order["order_filled_date"]))
|
||||
lines.append("({}d {}h {}m {}s from previous entry)"
|
||||
.format(days, hours, minutes, seconds))
|
||||
return lines
|
||||
|
||||
@authorized_only
|
||||
def _status(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@@ -392,37 +435,57 @@ class Telegram(RPCHandler):
|
||||
trade_ids = [int(i) for i in context.args if i.isnumeric()]
|
||||
|
||||
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
|
||||
|
||||
position_adjust = self._config.get('position_adjustment_enable', False)
|
||||
max_entries = self._config.get('max_entry_position_adjustment', -1)
|
||||
messages = []
|
||||
for r in results:
|
||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
||||
r['num_entries'] = len(r['filled_entry_orders'])
|
||||
r['sell_reason'] = r.get('sell_reason', "")
|
||||
lines = [
|
||||
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
||||
"*Trade ID:* `{trade_id}`" +
|
||||
("` (since {open_date_hum})`" if r['is_open'] else ""),
|
||||
"*Current Pair:* {pair}",
|
||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "",
|
||||
"*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "",
|
||||
"*Exit Reason:* `{sell_reason}`" if r['sell_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.extend([
|
||||
"*Open Rate:* `{open_rate:.8f}`",
|
||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||
"*Current Rate:* `{current_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 "",
|
||||
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
||||
+ "`{profit_ratio:.2%}`",
|
||||
]
|
||||
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
|
||||
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
||||
"`({initial_stop_loss_ratio:.2%})`")
|
||||
])
|
||||
|
||||
# Adding stoploss and stoploss percentage only if it is not None
|
||||
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
||||
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||
"`({stoploss_current_dist_ratio:.2%})`")
|
||||
if r['open_order']:
|
||||
if r['sell_order_status']:
|
||||
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
||||
else:
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
if r['is_open']:
|
||||
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
|
||||
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
||||
"`({initial_stop_loss_ratio:.2%})`")
|
||||
|
||||
# Adding stoploss and stoploss percentage only if it is not None
|
||||
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
||||
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||
"`({stoploss_current_dist_ratio:.2%})`")
|
||||
if r['open_order']:
|
||||
if r['sell_order_status']:
|
||||
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
||||
else:
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
|
||||
lines_detail = self._prepare_entry_details(
|
||||
r['filled_entry_orders'], r['base_currency'], r['is_open'])
|
||||
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else ""))
|
||||
|
||||
# Filter empty lines using list-comprehension
|
||||
messages.append("\n".join([line for line in lines if line]).format(**r))
|
||||
@@ -703,9 +766,9 @@ class Telegram(RPCHandler):
|
||||
duration_msg = tabulate(
|
||||
[
|
||||
['Wins', str(timedelta(seconds=durations['wins']))
|
||||
if durations['wins'] != 'N/A' else 'N/A'],
|
||||
if durations['wins'] is not None else 'N/A'],
|
||||
['Losses', str(timedelta(seconds=durations['losses']))
|
||||
if durations['losses'] != 'N/A' else 'N/A']
|
||||
if durations['losses'] is not None else 'N/A']
|
||||
],
|
||||
headers=['', 'Avg. Duration']
|
||||
)
|
||||
@@ -851,10 +914,11 @@ class Telegram(RPCHandler):
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _forcebuy_action(self, pair, price=None):
|
||||
try:
|
||||
self._rpc._rpc_forcebuy(pair, price)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
if pair != 'cancel':
|
||||
try:
|
||||
self._rpc._rpc_forcebuy(pair, price)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None:
|
||||
if update.callback_query:
|
||||
@@ -884,10 +948,13 @@ class Telegram(RPCHandler):
|
||||
self._forcebuy_action(pair, price)
|
||||
else:
|
||||
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
||||
pairs = [InlineKeyboardButton(text=pair, callback_data=pair) for pair in whitelist]
|
||||
pair_buttons = [
|
||||
InlineKeyboardButton(text=pair, callback_data=pair) for pair in sorted(whitelist)]
|
||||
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
|
||||
|
||||
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
|
||||
self._send_msg(msg="Which pair?",
|
||||
keyboard=self._layout_inline_keyboard(pairs))
|
||||
keyboard=buttons_aligned)
|
||||
|
||||
@authorized_only
|
||||
def _trades(self, update: Update, context: CallbackContext) -> None:
|
||||
@@ -1282,6 +1349,7 @@ class Telegram(RPCHandler):
|
||||
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
|
||||
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
||||
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
||||
"*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n"
|
||||
|
||||
"_Statistics_\n"
|
||||
"------------\n"
|
||||
@@ -1309,6 +1377,19 @@ class Telegram(RPCHandler):
|
||||
|
||||
self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
@authorized_only
|
||||
def _health(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /health
|
||||
Shows the last process timestamp
|
||||
"""
|
||||
try:
|
||||
health = self._rpc._health()
|
||||
message = f"Last process: `{health['last_process_loc']}`"
|
||||
self._send_msg(message)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _version(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
|
@@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.persistence.models import LocalTrade, Order
|
||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||
_create_and_merge_informative_pair,
|
||||
@@ -686,7 +687,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
return False
|
||||
|
||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||
def should_sell(self, trade: Trade, rate: float, current_time: datetime, buy: bool,
|
||||
sell: bool, low: float = None, high: float = None,
|
||||
force_stoploss: float = 0) -> SellCheckTuple:
|
||||
"""
|
||||
@@ -703,7 +704,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
trade.adjust_min_max_rates(high or current_rate, low or current_rate)
|
||||
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
current_time=current_time,
|
||||
current_profit=current_profit,
|
||||
force_stoploss=force_stoploss, low=low, high=high)
|
||||
|
||||
# Set current rate to high for backtesting sell
|
||||
@@ -713,7 +715,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# if buy signal and ignore_roi is set, we don't need to evaluate min_roi.
|
||||
roi_reached = (not (buy and self.ignore_roi_if_buy_signal)
|
||||
and self.min_roi_reached(trade=trade, current_profit=current_profit,
|
||||
current_time=date))
|
||||
current_time=current_time))
|
||||
|
||||
sell_signal = SellType.NONE
|
||||
custom_reason = ''
|
||||
@@ -729,8 +731,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
sell_signal = SellType.SELL_SIGNAL
|
||||
else:
|
||||
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
|
||||
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate,
|
||||
current_profit=current_profit)
|
||||
pair=trade.pair, trade=trade, current_time=current_time,
|
||||
current_rate=current_rate, current_profit=current_profit)
|
||||
if custom_reason:
|
||||
sell_signal = SellType.CUSTOM_SELL
|
||||
if isinstance(custom_reason, str):
|
||||
@@ -862,23 +864,22 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def ft_check_timed_out(self, side: str, trade: Trade, order: Dict,
|
||||
def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order,
|
||||
current_time: datetime) -> bool:
|
||||
"""
|
||||
FT Internal method.
|
||||
Check if timeout is active, and if the order is still open and timed out
|
||||
"""
|
||||
timeout = self.config.get('unfilledtimeout', {}).get(side)
|
||||
ordertime = arrow.get(order['datetime']).datetime
|
||||
if timeout is not None:
|
||||
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
|
||||
timeout_kwargs = {timeout_unit: -timeout}
|
||||
timeout_threshold = current_time + timedelta(**timeout_kwargs)
|
||||
timedout = (order['status'] == 'open' and order['side'] == side
|
||||
and ordertime < timeout_threshold)
|
||||
timedout = (order.status == 'open' and order.side == side
|
||||
and order.order_date_utc < timeout_threshold)
|
||||
if timedout:
|
||||
return True
|
||||
time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout
|
||||
time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout
|
||||
|
||||
return strategy_safe_wrapper(time_method,
|
||||
default_retval=False)(
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, NamedTuple
|
||||
from typing import Any, Dict, NamedTuple, Optional
|
||||
|
||||
import arrow
|
||||
|
||||
@@ -211,7 +211,7 @@ class Wallets:
|
||||
|
||||
return stake_amount
|
||||
|
||||
def get_trade_stake_amount(self, pair: str, edge=None) -> float:
|
||||
def get_trade_stake_amount(self, pair: str, edge=None, update: bool = True) -> float:
|
||||
"""
|
||||
Calculate stake amount for the trade
|
||||
:return: float: Stake amount
|
||||
@@ -219,7 +219,8 @@ class Wallets:
|
||||
"""
|
||||
stake_amount: float
|
||||
# Ensure wallets are uptodate.
|
||||
self.update()
|
||||
if update:
|
||||
self.update()
|
||||
val_tied_up = Trade.total_open_trades_stakes()
|
||||
available_amount = self.get_available_stake_amount()
|
||||
|
||||
@@ -238,14 +239,15 @@ class Wallets:
|
||||
|
||||
return self._check_available_stake_amount(stake_amount, available_amount)
|
||||
|
||||
def validate_stake_amount(self, pair, stake_amount, min_stake_amount):
|
||||
def validate_stake_amount(
|
||||
self, pair: str, stake_amount: Optional[float], min_stake_amount: Optional[float]):
|
||||
if not stake_amount:
|
||||
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
|
||||
return 0
|
||||
|
||||
max_stake_amount = self.get_available_stake_amount()
|
||||
|
||||
if min_stake_amount > max_stake_amount:
|
||||
if min_stake_amount is not None and min_stake_amount > max_stake_amount:
|
||||
if self._log:
|
||||
logger.warning("Minimum stake amount > available balance.")
|
||||
return 0
|
||||
|
Reference in New Issue
Block a user