Merge branch 'freqtrade:develop' into plot_hyperopt_stats

This commit is contained in:
Italo
2022-02-25 17:58:57 +00:00
committed by GitHub
31 changed files with 488 additions and 217 deletions

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = 'develop'
__version__ = '2022.1'
if __version__ == 'develop':

View File

@@ -106,15 +106,18 @@ class Ftx(Exchange):
if order[0].get('status') == 'closed':
# Trigger order was triggered ...
real_order_id = order[0].get('info', {}).get('orderId')
# OrderId may be None for stoploss-market orders
# But contains "average" in these cases.
if real_order_id:
order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1)
# Fake type to stop - as this was really a stop order.
order1['id_stop'] = order1['id']
order1['id'] = order_id
order1['type'] = 'stop'
order1['status_stop'] = 'triggered'
return order1
order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1)
# Fake type to stop - as this was really a stop order.
order1['id_stop'] = order1['id']
order1['id'] = order_id
order1['type'] = 'stop'
order1['status_stop'] = 'triggered'
return order1
return order[0]
else:
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")

View File

@@ -979,10 +979,10 @@ class FreqtradeBot(LoggingMixin):
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 = 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)
if max_timeouts > 0 and canceled_count >= max_timeouts:
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
f'timed out {max_timeouts} times.')
try:
@@ -1021,12 +1021,12 @@ class FreqtradeBot(LoggingMixin):
# Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
filled_val = order.get('filled', 0.0) or 0.0
filled_val: float = order.get('filled', 0.0) or 0.0
filled_stake = filled_val * trade.open_rate
minstake = self.exchange.get_min_pair_stake_amount(
trade.pair, trade.open_rate, self.strategy.stoploss)
if filled_val > 0 and filled_stake < minstake:
if filled_val > 0 and minstake and filled_stake < minstake:
logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
f"as the filled amount of {filled_val} would result in an unsellable trade.")
@@ -1079,11 +1079,12 @@ class FreqtradeBot(LoggingMixin):
reason=reason)
return was_trade_fully_canceled
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
"""
Sell cancel - cancel order and update trade
:return: Reason for cancel
:return: True if exit order was cancelled, false otherwise
"""
cancelled = False
# if trade is not partially completed, just cancel the order
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
if not self.exchange.check_order_canceled_empty(order):
@@ -1094,7 +1095,7 @@ class FreqtradeBot(LoggingMixin):
trade.update_order(co)
except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
return 'error cancelling order'
return False
logger.info('Sell order %s for %s.', reason, trade)
else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
@@ -1108,9 +1109,11 @@ class FreqtradeBot(LoggingMixin):
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
cancelled = True
else:
# TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
cancelled = False
self.wallets.update()
self._notify_exit_cancel(
@@ -1118,7 +1121,7 @@ class FreqtradeBot(LoggingMixin):
order_type=self.strategy.order_types['sell'],
reason=reason
)
return reason
return cancelled
def _safe_exit_amount(self, pair: str, amount: float) -> float:
"""
@@ -1357,9 +1360,14 @@ class FreqtradeBot(LoggingMixin):
# Handling of this will happen in check_handle_timedout.
return True
order = self.handle_order_fee(trade, order)
order_obj = trade.select_order_by_order_id(order_id)
if not order_obj:
raise DependencyException(
f"Order_obj not found for {order_id}. This should not have happened.")
self.handle_order_fee(trade, order_obj, order)
trade.update(order)
trade.update_trade(order_obj)
# TODO: is the below necessary? it's already done in update_trade for filled buys
trade.recalc_trade_from_orders()
Trade.commit()
@@ -1411,17 +1419,15 @@ class FreqtradeBot(LoggingMixin):
return real_amount
return amount
def handle_order_fee(self, trade: Trade, order: Dict[str, Any]) -> Dict[str, Any]:
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order)
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount
order.pop('filled', None)
order_obj.ft_fee_base = trade.amount - new_amount
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
return order
def get_real_amount(self, trade: Trade, order: Dict) -> float:
"""

View File

@@ -29,18 +29,23 @@ def decimals_per_coin(coin: str):
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
def round_coin_value(value: float, coin: str, show_coin_name=True) -> str:
def round_coin_value(
value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str:
"""
Get price value for this coin
:param value: Value to be printed
:param coin: Which coin are we printing the price / value for
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
:return: Formatted / rounded value (with or without coin name)
"""
val = f"{value:.{decimals_per_coin(coin)}f}"
if not keep_trailing_zeros:
val = val.rstrip('0').rstrip('.')
if show_coin_name:
return f"{value:.{decimals_per_coin(coin)}f} {coin}"
else:
return f"{value:.{decimals_per_coin(coin)}f}"
val = f"{val} {coin}"
return val
def shorten_date(_date: str) -> str:

View File

@@ -128,7 +128,8 @@ class Backtesting:
def __del__(self):
self.cleanup()
def cleanup(self):
@staticmethod
def cleanup():
LoggingMixin.show_output = True
PairLocks.use_db = True
Trade.use_db = True
@@ -357,6 +358,18 @@ class Backtesting:
# use Open rate if open_rate > calculated sell rate
return sell_row[OPEN_IDX]
if (
trade_dur == 0
# Red candle (for longs), TODO: green candle (for shorts)
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle
and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate
and close_rate > sell_row[CLOSE_IDX]
):
# ROI on opening candles with custom pricing can only
# trigger if the entry was at Open or lower.
# details: https: // github.com/freqtrade/freqtrade/issues/6261
# If open_rate is < open, only allow sells below the close on red candles.
raise ValueError("Opening candle ROI on red candles.")
# 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.
@@ -414,7 +427,10 @@ class Backtesting:
trade.close_date = sell_candle_time
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
try:
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
except ValueError:
return None
# 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']

View File

@@ -373,7 +373,7 @@ class HyperoptTools():
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
lambda x: "{} {}".format(
round_coin_value(x['max_drawdown_abs'], stake_currency),
round_coin_value(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True),
(f"({x['max_drawdown_account']:,.2%})"
if has_account_drawdown
else f"({x['max_drawdown']:,.2%})"
@@ -388,7 +388,7 @@ class HyperoptTools():
trials['Profit'] = trials.apply(
lambda x: '{} {}'.format(
round_coin_value(x['Total profit'], stake_currency),
round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True),
f"({x['Profit']:,.2%})".rjust(10, ' ')
).rjust(25+len(stake_currency))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),

View File

@@ -57,7 +57,7 @@ def set_sequence_ids(engine, order_id, trade_id):
def migrate_trades_and_orders_table(
decl_base, inspector, engine,
trade_back_name: str, cols: List,
order_back_name: str):
order_back_name: str, cols_order: List):
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')
@@ -141,7 +141,7 @@ def migrate_trades_and_orders_table(
from {trade_back_name}
"""))
migrate_orders_table(engine, order_back_name, cols)
migrate_orders_table(engine, order_back_name, cols_order)
set_sequence_ids(engine, order_id, trade_id)
@@ -171,21 +171,30 @@ def drop_orders_table(engine, table_back_name: str):
connection.execute(text("drop table orders"))
def migrate_orders_table(engine, table_back_name: str, cols: List):
def migrate_orders_table(engine, table_back_name: str, cols_order: List):
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
# let SQLAlchemy create the schema as required
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,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
order_date, order_filled_date, order_update_date)
order_date, order_filled_date, order_update_date, ft_fee_base)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, null average, remaining, cost,
order_date, order_filled_date, order_update_date
order_date, order_filled_date, order_update_date, {ft_fee_base}
from {table_back_name}
"""))
def set_sqlite_to_wal(engine):
if engine.name == 'sqlite' and str(engine.url) != 'sqlite://':
# Set Mode to
with engine.begin() as connection:
connection.execute(text("PRAGMA journal_mode=wal"))
def check_migrate(engine, decl_base, previous_tables) -> None:
"""
Checks if migration is necessary and migrates if necessary
@@ -193,6 +202,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
inspector = inspect(engine)
cols = inspector.get_columns('trades')
cols_orders = inspector.get_columns('orders')
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')
@@ -200,15 +210,14 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Check if migration necessary
# Migrates both trades and orders table!
if not has_column(cols, 'buy_tag'):
# if not has_column(cols, 'buy_tag'):
if 'orders' not in previous_tables or not has_column(cols_orders, 'ft_fee_base'):
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')
decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders)
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)
set_sqlite_to_wal(engine)

View File

@@ -16,7 +16,6 @@ from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate
@@ -39,6 +38,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
"""
kwargs = {}
if db_url == 'sqlite:///':
raise OperationalException(
f'Bad db-url {db_url}. For in-memory database, please use `sqlite://`.')
if db_url == 'sqlite://':
kwargs.update({
'poolclass': StaticPool,
@@ -113,14 +115,15 @@ class Order(_DECL_BASE):
trade = relationship("Trade", back_populates="orders")
ft_order_side = Column(String(25), nullable=False)
ft_pair = Column(String(25), nullable=False)
# order_side can only be 'buy', 'sell' or 'stoploss'
ft_order_side: str = Column(String(25), nullable=False)
ft_pair: str = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True)
order_type = Column(String(50), nullable=True)
order_type: str = Column(String(50), nullable=True)
side = Column(String(25), nullable=True)
price = Column(Float, nullable=True)
average = Column(Float, nullable=True)
@@ -132,10 +135,29 @@ class Order(_DECL_BASE):
order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True)
ft_fee_base = Column(Float, nullable=True)
@property
def order_date_utc(self):
def order_date_utc(self) -> datetime:
""" Order-date with UTC timezoneinfo"""
return self.order_date.replace(tzinfo=timezone.utc)
@property
def safe_price(self) -> float:
return self.average or self.price
@property
def safe_filled(self) -> float:
return self.filled or self.amount or 0.0
@property
def safe_fee_base(self) -> float:
return self.ft_fee_base or 0.0
@property
def safe_amount_after_fee(self) -> float:
return self.safe_filled - self.safe_fee_base
def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
@@ -452,40 +474,39 @@ class LocalTrade():
f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
def update(self, order: Dict) -> None:
def update_trade(self, order: Order) -> None:
"""
Updates this entity with amount and actual open/close rates.
:param order: order retrieved by exchange.fetch_order()
:return: None
"""
order_type = order['type']
# Ignore open and cancelled orders
if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None:
if order.status == 'open' or order.safe_price is None:
return
logger.info('Updating trade (id=%s) ...', self.id)
logger.info(f'Updating trade (id={self.id}) ...')
if order_type in ('market', 'limit') and order['side'] == 'buy':
if order.ft_order_side == 'buy':
# Update open rate and actual amount
self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
self.open_rate = order.safe_price
self.amount = order.safe_amount_after_fee
if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.')
self.open_order_id = None
self.recalc_trade_from_orders()
elif order_type in ('market', 'limit') and order['side'] == 'sell':
elif order.ft_order_side == 'sell':
if self.is_open:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price'))
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'):
logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.')
self.close(order.safe_price)
elif order.ft_order_side == 'stoploss':
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
if self.is_open:
logger.info(f'{order_type.upper()} is hit for {self}.')
self.close(safe_value_fallback(order, 'average', 'price'))
logger.info(f'{order.order_type.upper()} is hit for {self}.')
self.close(order.safe_price)
else:
raise ValueError(f'Unknown order type: {order_type}')
raise ValueError(f'Unknown order type: {order.order_type}')
Trade.commit()
def close(self, rate: float, *, show_msg: bool = True) -> None:
@@ -628,7 +649,7 @@ class LocalTrade():
(o.status not in NON_OPEN_EXCHANGE_STATES)):
continue
tmp_amount = o.amount
tmp_amount = o.safe_amount_after_fee
tmp_price = o.average or o.price
if o.filled is not None:
tmp_amount = o.filled
@@ -799,11 +820,11 @@ class Trade(_DECL_BASE, LocalTrade):
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String(25), nullable=True)
open_rate = Column(Float)
open_rate: float = Column(Float)
open_rate_requested = Column(Float)
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float)
close_rate = Column(Float)
close_rate: Optional[float] = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
close_profit_abs = Column(Float)

View File

@@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
from freqtrade.rpc.api_server.deps import get_config
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException
@@ -20,8 +20,9 @@ router = APIRouter()
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
# flake8: noqa: C901
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config)):
config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
"""Start backtesting if not done so already"""
if ApiServer._bgtask_running:
raise RPCException('Bot Background task already running')
@@ -120,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_get_backtest():
def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
"""
Get backtesting result.
Returns Result after backtesting has been ran.
@@ -156,7 +157,7 @@ def api_get_backtest():
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_delete_backtest():
def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"""Reset backtesting"""
if ApiServer._bgtask_running:
return {
@@ -182,7 +183,7 @@ def api_delete_backtest():
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_abort():
def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
if not ApiServer._bgtask_running:
return {
"status": "not_running",

View File

@@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional
from fastapi import Depends
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException
@@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)):
ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config)
return ApiServer._exchange
def is_webserver_mode(config=Depends(get_config)):
if config['runmode'] != RunMode.WEBSERVER:
raise RPCException('Bot is not in the correct state')
return None

View File

@@ -599,11 +599,6 @@ class RPC:
'est_stake': est_stake or 0,
'stake': stake_currency,
})
if total == 0.0:
if self._freqtrade.config['dry_run']:
raise RPCException('Running in Dry Run, balances are not available.')
else:
raise RPCException('All balances are zero.')
value = self._fiat_converter.convert_amount(
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0

View File

@@ -790,12 +790,13 @@ class Telegram(RPCHandler):
output = ''
if self._config['dry_run']:
output += "*Warning:* Simulated balances in Dry Mode.\n"
output += ("Starting capital: "
f"`{result['starting_capital']}` {self._config['stake_currency']}"
)
output += (f" `{result['starting_capital_fiat']}` "
f"{self._config['fiat_display_currency']}.\n"
starting_cap = round_coin_value(
result['starting_capital'], self._config['stake_currency'])
output += f"Starting capital: `{starting_cap}`"
starting_cap_fiat = round_coin_value(
result['starting_capital_fiat'], self._config['fiat_display_currency']
) if result['starting_capital_fiat'] > 0 else ''
output += (f" `, {starting_cap_fiat}`.\n"
) if result['starting_capital_fiat'] > 0 else '.\n'
total_dust_balance = 0