Merge branch 'develop' into feat/short

This commit is contained in:
Matthias 2022-02-23 06:27:56 +01:00
commit 8952829adc
29 changed files with 252 additions and 202 deletions

View File

@ -8,6 +8,7 @@
"amend_last_stake_amount": false, "amend_last_stake_amount": false,
"last_stake_amount_min_ratio": 0.5, "last_stake_amount_min_ratio": 0.5,
"dry_run": true, "dry_run": true,
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"timeframe": "5m", "timeframe": "5m",
"trailing_stop": false, "trailing_stop": false,

View File

@ -508,6 +508,46 @@ class MyAwesomeStrategy(IStrategy):
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization. You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
### Optimizing `max_entry_position_adjustment`
While `max_entry_position_adjustment` is not a separate space, it can still be used in hyperopt by using the property approach shown above.
``` python
from pandas import DataFrame
from functools import reduce
import talib.abstract as ta
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter)
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy):
stoploss = -0.05
timeframe = '15m'
# Define the parameter spaces
max_epa = CategoricalParameter([-1, 0, 1, 3, 5, 10], default=1, space="buy", optimize=True)
@property
def max_entry_position_adjustment(self):
return self.max_epa.value
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ...
```
??? Tip "Using `IntParameter`"
You can also use the `IntParameter` for this optimization, but you must explicitly return an integer:
``` python
max_epa = IntParameter(-1, 10, default=1, space="buy", optimize=True)
@property
def max_entry_position_adjustment(self):
return int(self.max_epa.value)
```
## Loss-functions ## Loss-functions
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.

View File

@ -246,7 +246,7 @@ On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to > 0. This option is disabled by default, and will only apply if set to > 0.
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. For `PriceFilter` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
Calculation example: Calculation example:

View File

@ -1,4 +1,4 @@
mkdocs==1.2.3 mkdocs==1.2.3
mkdocs-material==8.1.10 mkdocs-material==8.2.1
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.1 pymdown-extensions==9.2

View File

@ -2,6 +2,7 @@
The `stoploss` configuration parameter is loss as ratio that should trigger a sale. The `stoploss` configuration parameter is loss as ratio that should trigger a sale.
For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional.
Stoploss calculations do include fees, so a stoploss of -10% is placed exactly 10% below the entry point.
Most of the strategy files already include the optimal `stoploss` value. Most of the strategy files already include the optimal `stoploss` value.
@ -30,7 +31,7 @@ These modes can be configured with these values:
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
Enable or Disable stop loss on exchange. Enable or Disable stop loss on exchange.
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order fills. This will protect you against sudden crashes in market, as the order execution happens purely within the exchange, and has no potential network overhead.
If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this. `stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this.

View File

@ -115,7 +115,9 @@ class Ftx(Exchange):
if order[0].get('status') == 'closed': if order[0].get('status') == 'closed':
# Trigger order was triggered ... # Trigger order was triggered ...
real_order_id = order[0].get('info', {}).get('orderId') 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) order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1) self._log_exchange_response('fetch_stoploss_order1', order1)
# Fake type to stop - as this was really a stop order. # Fake type to stop - as this was really a stop order.
@ -124,6 +126,7 @@ class Ftx(Exchange):
order1['type'] = 'stop' order1['type'] = 'stop'
order1['status_stop'] = 'triggered' order1['status_stop'] = 'triggered'
return order1 return order1
return order[0] return order[0]
else: else:
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}") raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")

View File

@ -344,29 +344,6 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(trade, order.order_id, send_msg=False) self.update_trade_state(trade, order.order_id, send_msg=False)
def handle_insufficient_funds(self, trade: Trade): def handle_insufficient_funds(self, trade: Trade):
"""
Determine if we ever opened a exiting order for this trade.
If not, try update entering fees - otherwise "refind" the open order we obviously lost.
"""
exit_order = trade.select_order(trade.exit_side, None)
if exit_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 {trade.enter_side} fees for {trade}")
order = trade.select_order(trade.enter_side, False)
if order:
logger.info(
f"Updating {trade.enter_side}-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. Try refinding a lost trade.
Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy). Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
@ -379,9 +356,6 @@ class FreqtradeBot(LoggingMixin):
if not order.ft_is_open: if not order.ft_is_open:
logger.debug(f"Order {order} is no longer open.") logger.debug(f"Order {order} is no longer open.")
continue continue
if order.ft_order_side == trade.enter_side:
# Skip buy side - this is handled by reupdate_enter_order_fees
continue
try: try:
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss') order.ft_order_side == 'stoploss')
@ -393,6 +367,9 @@ class FreqtradeBot(LoggingMixin):
if fo and fo['status'] == 'open': if fo and fo['status'] == 'open':
# Assume this as the open order # Assume this as the open order
trade.open_order_id = order.order_id trade.open_order_id = order.order_id
elif order.ft_order_side == trade.enter_side:
if fo and fo['status'] == 'open':
trade.open_order_id = order.order_id
if fo: if fo:
logger.info(f"Found {order} for trade {trade}.") logger.info(f"Found {order} for trade {trade}.")
self.update_trade_state(trade, order.order_id, fo, self.update_trade_state(trade, order.order_id, fo,
@ -1241,12 +1218,12 @@ class FreqtradeBot(LoggingMixin):
# Cancelled orders may have the status of 'canceled' or 'closed' # Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: 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 filled_stake = filled_val * trade.open_rate
minstake = self.exchange.get_min_pair_stake_amount( minstake = self.exchange.get_min_pair_stake_amount(
trade.pair, trade.open_rate, self.strategy.stoploss) 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( logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, " f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
f"as the filled amount of {filled_val} would result in an unexitable trade.") f"as the filled amount of {filled_val} would result in an unexitable trade.")

View File

@ -29,18 +29,23 @@ def decimals_per_coin(coin: str):
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) 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 Get price value for this coin
:param value: Value to be printed :param value: Value to be printed
:param coin: Which coin are we printing the price / value for :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 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) :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: if show_coin_name:
return f"{value:.{decimals_per_coin(coin)}f} {coin}" val = f"{val} {coin}"
else:
return f"{value:.{decimals_per_coin(coin)}f}" return val
def shorten_date(_date: str) -> str: def shorten_date(_date: str) -> str:

View File

@ -408,6 +408,18 @@ class Backtesting:
# use Open rate if open_rate > calculated sell rate # use Open rate if open_rate > calculated sell rate
return sell_row[OPEN_IDX] 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 # Use the maximum between close_rate and low as we
# cannot sell outside of a candle. # cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that. # Applies when a new ROI setting comes in place and the whole candle is above that.
@ -471,7 +483,10 @@ class Backtesting:
trade.close_date = sell_candle_time trade.close_date = sell_candle_time
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
try:
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) 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 # call the custom exit price,with default value as previous closerate
current_profit = trade.calc_profit_ratio(closerate) current_profit = trade.calc_profit_ratio(closerate)
order_type = self.strategy.order_types['sell'] 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( trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
lambda x: "{} {}".format( 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%})" (f"({x['max_drawdown_account']:,.2%})"
if has_account_drawdown if has_account_drawdown
else f"({x['max_drawdown']:,.2%})" else f"({x['max_drawdown']:,.2%})"
@ -388,7 +388,7 @@ class HyperoptTools():
trials['Profit'] = trials.apply( trials['Profit'] = trials.apply(
lambda x: '{} {}'.format( 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, ' ') f"({x['Profit']:,.2%})".rjust(10, ' ')
).rjust(25+len(stake_currency)) ).rjust(25+len(stake_currency))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),

View File

@ -208,6 +208,13 @@ def migrate_orders_table(engine, table_back_name: str, cols: List):
""")) """))
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: def check_migrate(engine, decl_base, previous_tables) -> None:
""" """
Checks if migration is necessary and migrates if necessary Checks if migration is necessary and migrates if necessary
@ -235,3 +242,4 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
if 'orders' not in previous_tables and 'trades' in previous_tables: if 'orders' not in previous_tables and 'trades' in previous_tables:
logger.info('Moving open orders to Orders table.') logger.info('Moving open orders to Orders table.')
migrate_open_orders_to_trades(engine) migrate_open_orders_to_trades(engine)
set_sqlite_to_wal(engine)

View File

@ -40,6 +40,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
""" """
kwargs = {} kwargs = {}
if db_url == 'sqlite:///':
raise OperationalException(
f'Bad db-url {db_url}. For in-memory database, please use `sqlite://`.')
if db_url == 'sqlite://': if db_url == 'sqlite://':
kwargs.update({ kwargs.update({
'poolclass': StaticPool, 'poolclass': StaticPool,
@ -885,13 +888,13 @@ class LocalTrade():
self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]: self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]:
""" """
Finds latest order for this orderside and status 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? :param is_open: Only search for open orders?
:return: latest Order object if it exists, else None :return: latest Order object if it exists, else None
""" """
orders = self.orders orders = self.orders
if order_side: if order_side:
orders = [o for o in self.orders if o.side == order_side] orders = [o for o in self.orders if o.ft_order_side == order_side]
if is_open is not None: if is_open is not None:
orders = [o for o in orders if o.ft_is_open == is_open] orders = [o for o in orders if o.ft_is_open == is_open]
if len(orders) > 0: if len(orders) > 0:
@ -1025,11 +1028,11 @@ class Trade(_DECL_BASE, LocalTrade):
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True) fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String(25), 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_rate_requested = Column(Float)
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float) open_trade_value = Column(Float)
close_rate = Column(Float) close_rate: Optional[float] = Column(Float)
close_rate_requested = Column(Float) close_rate_requested = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
close_profit_abs = Column(Float) close_profit_abs = Column(Float)

View File

@ -20,6 +20,7 @@ router = APIRouter()
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
# flake8: noqa: C901
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config)): config=Depends(get_config)):
"""Start backtesting if not done so already""" """Start backtesting if not done so already"""
@ -32,6 +33,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
for setting in settings.keys(): for setting in settings.keys():
if settings[setting] is not None: if settings[setting] is not None:
btconfig[setting] = settings[setting] btconfig[setting] = settings[setting]
try:
btconfig['stake_amount'] = float(btconfig['stake_amount'])
except ValueError:
pass
# Force dry-run for backtesting # Force dry-run for backtesting
btconfig['dry_run'] = True btconfig['dry_run'] = True
@ -57,7 +62,6 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
): ):
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig) ApiServer._bt = Backtesting(btconfig)
if ApiServer._bt.timeframe_detail:
ApiServer._bt.load_bt_data_detail() ApiServer._bt.load_bt_data_detail()
else: else:
ApiServer._bt.config = btconfig ApiServer._bt.config = btconfig

View File

@ -152,7 +152,7 @@ class ShowConfig(BaseModel):
trading_mode: str trading_mode: str
short_allowed: bool short_allowed: bool
stake_currency: str stake_currency: str
stake_amount: Union[float, str] stake_amount: str
available_capital: Optional[float] available_capital: Optional[float]
stake_currency_decimals: int stake_currency_decimals: int
max_open_trades: int max_open_trades: int
@ -291,6 +291,7 @@ class ForceEnterPayload(BaseModel):
price: Optional[float] price: Optional[float]
ordertype: Optional[OrderTypeValues] ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float] stakeamount: Optional[float]
entry_tag: Optional[str]
class ForceExitPayload(BaseModel): class ForceExitPayload(BaseModel):
@ -380,7 +381,7 @@ class BacktestRequest(BaseModel):
timeframe_detail: Optional[str] timeframe_detail: Optional[str]
timerange: Optional[str] timerange: Optional[str]
max_open_trades: Optional[int] max_open_trades: Optional[int]
stake_amount: Optional[Union[float, str]] stake_amount: Optional[str]
enable_protections: bool enable_protections: bool
dry_run_wallet: Optional[float] dry_run_wallet: Optional[float]

View File

@ -141,9 +141,11 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount 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_force_entry(payload.pair, payload.price, order_side=payload.side, trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
order_type=ordertype, stake_amount=stake_amount) order_type=ordertype, stake_amount=stake_amount,
enter_tag=entry_tag)
if trade: if trade:
return ForceEnterResponse.parse_obj(trade.to_json()) return ForceEnterResponse.parse_obj(trade.to_json())

View File

@ -116,7 +116,7 @@ class RPC:
'short_allowed': config.get('trading_mode', 'spot') != 'spot', 'short_allowed': config.get('trading_mode', 'spot') != 'spot',
'stake_currency': config['stake_currency'], 'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(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'), 'available_capital': config.get('available_capital'),
'max_open_trades': (config['max_open_trades'] 'max_open_trades': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
@ -606,11 +606,6 @@ class RPC:
'est_stake': est_stake or 0, 'est_stake': est_stake or 0,
'stake': stake_currency, '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( value = self._fiat_converter.convert_amount(
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
@ -727,7 +722,8 @@ class RPC:
def _rpc_force_entry(self, pair: str, price: Optional[float], *, def _rpc_force_entry(self, pair: str, price: Optional[float], *,
order_type: Optional[str] = None, order_type: Optional[str] = None,
order_side: SignalDirection = SignalDirection.LONG, order_side: SignalDirection = SignalDirection.LONG,
stake_amount: Optional[float] = None) -> Optional[Trade]: stake_amount: Optional[float] = None,
enter_tag: Optional[str] = None) -> Optional[Trade]:
""" """
Handler for forcebuy <asset> <price> Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
@ -765,7 +761,8 @@ class RPC:
'forcebuy', self._freqtrade.strategy.order_types['buy']) 'forcebuy', self._freqtrade.strategy.order_types['buy'])
if self._freqtrade.execute_entry(pair, stake_amount, price, if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade, ordertype=order_type, trade=trade,
is_short=(order_side == SignalDirection.SHORT) is_short=(order_side == SignalDirection.SHORT),
enter_tag=enter_tag,
): ):
Trade.commit() Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()

View File

@ -813,12 +813,13 @@ class Telegram(RPCHandler):
output = '' output = ''
if self._config['dry_run']: if self._config['dry_run']:
output += "*Warning:* Simulated balances in Dry Mode.\n" output += "*Warning:* Simulated balances in Dry Mode.\n"
starting_cap = round_coin_value(
output += ("Starting capital: " result['starting_capital'], self._config['stake_currency'])
f"`{result['starting_capital']}` {self._config['stake_currency']}" output += f"Starting capital: `{starting_cap}`"
) starting_cap_fiat = round_coin_value(
output += (f" `{result['starting_capital_fiat']}` " result['starting_capital_fiat'], self._config['fiat_display_currency']
f"{self._config['fiat_display_currency']}.\n" ) if result['starting_capital_fiat'] > 0 else ''
output += (f" `, {starting_cap_fiat}`.\n"
) if result['starting_capital_fiat'] > 0 else '.\n' ) if result['starting_capital_fiat'] > 0 else '.\n'
total_dust_balance = 0 total_dust_balance = 0
@ -937,6 +938,7 @@ class Telegram(RPCHandler):
self._send_msg(str(e)) self._send_msg(str(e))
def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection): def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection):
if pair != 'cancel':
try: try:
self._rpc._rpc_force_entry(pair, price, order_side=order_side) self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e: except RPCException as e:
@ -975,12 +977,13 @@ class Telegram(RPCHandler):
whitelist = self._rpc._rpc_whitelist()['whitelist'] whitelist = self._rpc._rpc_whitelist()['whitelist']
pair_buttons = [ pair_buttons = [
InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}") InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}")
for pair in whitelist 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?", self._send_msg(msg="Which pair?",
keyboard=self._layout_inline_keyboard(pair_buttons), keyboard=buttons_aligned,
callback_path="update_forcelong",
query=update.callback_query) query=update.callback_query)
@authorized_only @authorized_only

View File

@ -800,7 +800,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return False return False
def should_exit(self, trade: Trade, rate: float, date: datetime, *, def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
enter: bool, exit_: bool, enter: bool, exit_: bool,
low: float = None, high: float = None, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> SellCheckTuple:
@ -819,7 +819,8 @@ class IStrategy(ABC, HyperStrategyMixin):
trade.adjust_min_max_rates(high or current_rate, low or current_rate) trade.adjust_min_max_rates(high or current_rate, low or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, 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) force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting sell # Set current rate to high for backtesting sell
@ -829,7 +830,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi. # if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (enter and self.ignore_roi_if_buy_signal) roi_reached = (not (enter and self.ignore_roi_if_buy_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date)) current_time=current_time))
sell_signal = SellType.NONE sell_signal = SellType.NONE
custom_reason = '' custom_reason = ''
@ -846,8 +847,8 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
trade_type = "exit_short" if trade.is_short else "sell" trade_type = "exit_short" if trade.is_short else "sell"
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, pair=trade.pair, trade=trade, current_time=current_time,
current_profit=current_profit) current_rate=current_rate, current_profit=current_profit)
if custom_reason: if custom_reason:
sell_signal = SellType.CUSTOM_SELL sell_signal = SellType.CUSTOM_SELL
if isinstance(custom_reason, str): if isinstance(custom_reason, str):

View File

@ -7,8 +7,8 @@ coveralls==3.3.1
flake8==4.0.1 flake8==4.0.1
flake8-tidy-imports==4.6.0 flake8-tidy-imports==4.6.0
mypy==0.931 mypy==0.931
pytest==7.0.0 pytest==7.0.1
pytest-asyncio==0.17.2 pytest-asyncio==0.18.1
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-mock==3.7.0 pytest-mock==3.7.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4
@ -17,12 +17,12 @@ isort==5.10.1
time-machine==2.6.0 time-machine==2.6.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.4.1 nbconvert==6.4.2
# mypy types # mypy types
types-cachetools==4.2.9 types-cachetools==4.2.9
types-filelock==3.2.5 types-filelock==3.2.5
types-requests==2.27.8 types-requests==2.27.10
types-tabulate==0.8.5 types-tabulate==0.8.5
# Extensions to datetime library # Extensions to datetime library

View File

@ -5,6 +5,6 @@
scipy==1.8.0 scipy==1.8.0
scikit-learn==1.0.2 scikit-learn==1.0.2
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.4.2 filelock==3.6.0
joblib==1.1.0 joblib==1.1.0
progressbar2==4.0.0 progressbar2==4.0.0

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.5.0 plotly==5.6.0

View File

@ -1,8 +1,8 @@
numpy==1.22.2 numpy==1.22.2
pandas==1.4.0 pandas==1.4.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.73.17 ccxt==1.73.70
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.1 cryptography==36.0.1
aiohttp==3.8.1 aiohttp==3.8.1
@ -25,13 +25,13 @@ blosc==1.10.6
py_find_1st==1.1.5 py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.5 python-rapidjson==1.6
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.73.0 fastapi==0.74.0
uvicorn==0.17.4 uvicorn==0.17.4
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0
@ -41,7 +41,7 @@ psutil==5.9.0
colorama==0.4.4 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.26 prompt-toolkit==3.0.28
# Extensions to datetime library # Extensions to datetime library
python-dateutil==2.8.2 python-dateutil==2.8.2

View File

@ -174,7 +174,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side):
assert not exchange.stoploss_adjust(sl3, order, side=side) assert not exchange.stoploss_adjust(sl3, order, side=side)
def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_order): def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order, limit_buy_order):
default_conf['dry_run'] = True default_conf['dry_run'] = True
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
@ -196,9 +196,15 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': 'closed'}]) # stoploss Limit order
api_mock.fetch_orders = MagicMock(return_value=[
{'id': 'X', 'status': 'closed',
'info': {
'orderId': 'mocked_limit_sell',
}}])
api_mock.fetch_order = MagicMock(return_value=limit_sell_order) api_mock.fetch_order = MagicMock(return_value=limit_sell_order)
# No orderId field - no call to fetch_order
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
assert resp assert resp
assert api_mock.fetch_order.call_count == 1 assert api_mock.fetch_order.call_count == 1
@ -207,15 +213,16 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_
assert resp['type'] == 'stop' assert resp['type'] == 'stop'
assert resp['status_stop'] == 'triggered' assert resp['status_stop'] == 'triggered'
api_mock.fetch_order = MagicMock(return_value=limit_buy_order) # Stoploss market order
# Contains no new Order, but "average" instead
order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254}
api_mock.fetch_orders = MagicMock(return_value=[order])
api_mock.fetch_order.reset_mock()
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
assert resp assert resp
assert api_mock.fetch_order.call_count == 1 # fetch_order not called (no regular order ID)
assert resp['id_stop'] == 'mocked_limit_buy' assert api_mock.fetch_order.call_count == 0
assert resp['id'] == 'X' assert order == order
assert resp['type'] == 'stop'
assert resp['status_stop'] == 'triggered'
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))

View File

@ -562,25 +562,39 @@ tc35 = BTContainer(data=[
) )
# Test 36: Custom-entry-price around candle low # Test 36: Custom-entry-price around candle low
# Causes immediate ROI exit. This is currently expected behavior (#6261) # Would cause immediate ROI exit, but since the trade was entered
# https://github.com/freqtrade/freqtrade/issues/6261 # below open, we treat this as cheating, and delay the sell by 1 candle.
# But may change at a later point. # details: https://github.com/freqtrade/freqtrade/issues/6261
tc36 = BTContainer(data=[ tc36 = BTContainer(data=[
# D O H L C V B S BT # D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Enter and immediate ROI [1, 5000, 5500, 4951, 4999, 6172, 0, 0], # Enter and immediate ROI
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], [2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], [3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.1, stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01,
custom_entry_price=4952,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)]
)
# Test 37: Custom-entry-price around candle low
# Would cause immediate ROI exit below close
# details: https://github.com/freqtrade/freqtrade/issues/6261
tc37 = BTContainer(data=[
# D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5400, 5500, 4951, 5100, 6172, 0, 0], # Enter and immediate ROI
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01,
custom_entry_price=4952, custom_entry_price=4952,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)]
) )
# Test 38: Custom exit price below all candles
# Test 37: Custom exit price below all candles
# Price adjusted to candle Low. # Price adjusted to candle Low.
tc37 = BTContainer(data=[ tc38 = BTContainer(data=[
# D O H L C V B S BT # D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0],
@ -593,9 +607,9 @@ tc37 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)]
) )
# Test 38: Custom exit price above all candles # Test 39: Custom exit price above all candles
# causes sell signal timeout # causes sell signal timeout
tc38 = BTContainer(data=[ tc39 = BTContainer(data=[
# D O H L C V B S BT # D O H L C V B S BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0],

View File

@ -776,7 +776,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01) mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
assert ('∙ `-0.00000500 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`' assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
in msg_mock.call_args_list[-1][0][0]) in msg_mock.call_args_list[-1][0][0])
msg_mock.reset_mock() msg_mock.reset_mock()
@ -852,7 +852,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
assert '*XRP:*' not in result assert '*XRP:*' not in result
assert 'Balance:' in result assert 'Balance:' in result
assert 'Est. BTC:' in result assert 'Est. BTC:' in result
assert 'BTC: 12.00000000' in result assert 'BTC: 12' in result
assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert "*3 Other Currencies (< 0.0001 BTC):*" in result
assert 'BTC: 0.00000309' in result assert 'BTC: 0.00000309' in result
@ -868,7 +868,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
telegram._balance(update=update, context=MagicMock()) telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'All balances are zero.' in result assert 'Starting capital: `0 BTC' in result
def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None:
@ -881,7 +881,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert "*Warning:* Simulated balances in Dry Mode." in result assert "*Warning:* Simulated balances in Dry Mode." in result
assert "Starting capital: `1000` BTC" in result assert "Starting capital: `1000 BTC`" in result
def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
@ -1277,7 +1277,8 @@ def test_forceenter_no_pair(default_conf, update, mocker) -> None:
assert msg_mock.call_args_list[0][1]['msg'] == 'Which pair?' assert msg_mock.call_args_list[0][1]['msg'] == 'Which pair?'
# assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' # assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy'
keyboard = msg_mock.call_args_list[0][1]['keyboard'] keyboard = msg_mock.call_args_list[0][1]['keyboard']
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 # One additional button - cancel
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5
update = MagicMock() update = MagicMock()
update.callback_query = MagicMock() update.callback_query = MagicMock()
update.callback_query.data = 'XRP/USDT_||_long' update.callback_query.data = 'XRP/USDT_||_long'
@ -1760,7 +1761,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
'leverage': leverage, 'leverage': leverage,
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'stake_amount': 0.001, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
@ -1780,7 +1781,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
f'{leverage_text}' f'{leverage_text}'
'*Open Rate:* `0.00001099`\n' '*Open Rate:* `0.00001099`\n'
'*Current Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n'
'*Total:* `(0.00100000 BTC, 12.345 USD)`' '*Total:* `(0.01465333 BTC, 180.895 USD)`'
) )
freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'}
@ -1865,7 +1866,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': leverage, 'leverage': leverage,
'stake_amount': 0.001, 'stake_amount': 0.01465333,
# 'stake_amount_fiat': 0.0, # 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
@ -1880,7 +1881,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
f"{leverage_text}" f"{leverage_text}"
'*Open Rate:* `0.00001099`\n' '*Open Rate:* `0.00001099`\n'
'*Total:* `(0.00100000 BTC, 12.345 USD)`' '*Total:* `(0.01465333 BTC, 180.895 USD)`'
) )
@ -2095,7 +2096,7 @@ def test_send_msg_buy_notification_no_fiat(
'leverage': leverage, 'leverage': leverage,
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'stake_amount': 0.001, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': None, 'fiat_currency': None,
@ -2112,7 +2113,7 @@ def test_send_msg_buy_notification_no_fiat(
f'{leverage_text}' f'{leverage_text}'
'*Open Rate:* `0.00001099`\n' '*Open Rate:* `0.00001099`\n'
'*Current Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n'
'*Total:* `(0.00100000 BTC)`' '*Total:* `(0.01465333 BTC)`'
) )

View File

@ -445,7 +445,8 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
strategy.custom_stoploss = custom_stop strategy.custom_stoploss = custom_stop
now = arrow.utcnow().datetime now = arrow.utcnow().datetime
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, current_rate = trade.open_rate * (1 + profit)
sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=now, current_profit=profit, current_time=now, current_profit=profit,
force_stoploss=0, high=None) force_stoploss=0, high=None)
assert isinstance(sl_flag, SellCheckTuple) assert isinstance(sl_flag, SellCheckTuple)
@ -455,8 +456,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
else: else:
assert sl_flag.sell_flag is True assert sl_flag.sell_flag is True
assert round(trade.stop_loss, 2) == adjusted assert round(trade.stop_loss, 2) == adjusted
current_rate2 = trade.open_rate * (1 + profit2)
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade,
current_time=now, current_profit=profit2, current_time=now, current_profit=profit2,
force_stoploss=0, high=None) force_stoploss=0, high=None)
assert sl_flag.sell_type == expected2 assert sl_flag.sell_type == expected2

View File

@ -2730,8 +2730,7 @@ def test_check_handle_timedout_exception(default_conf_usdt, ticker_usdt, open_tr
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_short) -> None:
is_short) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
l_order = limit_order[enter_side(is_short)] l_order = limit_order[enter_side(is_short)]
@ -2775,6 +2774,9 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order,
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
assert log_has_re(r"Order .* for .* not cancelled.", caplog) assert log_has_re(r"Order .* for .* not cancelled.", caplog)
# min_pair_stake empty should not crash
mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None)
assert not freqtrade.handle_cancel_enter(trade, limit_order[enter_side(is_short)], reason)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -4609,23 +4611,17 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog, is_sh
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
create_mock_trades(fee, is_short=is_short) mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
return_value={'status': 'open'})
create_mock_trades(fee, is_short)
trades = Trade.get_trades().all() trades = Trade.get_trades().all()
freqtrade.reupdate_enter_order_fees(trades[0]) freqtrade.handle_insufficient_funds(trades[3])
assert log_has_re( # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
f"Trying to reupdate {enter_side(is_short)} "
r"fees for .*",
caplog
)
assert mock_uts.call_count == 1 assert mock_uts.call_count == 1
assert mock_uts.call_args_list[0][0][0] == trades[0] assert mock_uts.call_args_list[0][0][0] == trades[3]
assert mock_uts.call_args_list[0][0][1] == mock_order_1(is_short=is_short)['id'] assert mock_uts.call_args_list[0][0][1] == mock_order_4(is_short)['id']
assert log_has_re( assert log_has_re(r"Trying to refind lost order for .*", caplog)
f"Updating {enter_side(is_short)}-fee on trade "
r".* for order .*\.",
caplog
)
mock_uts.reset_mock() mock_uts.reset_mock()
caplog.clear() caplog.clear()
@ -4644,55 +4640,14 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog, is_sh
) )
Trade.query.session.add(trade) Trade.query.session.add(trade)
freqtrade.reupdate_enter_order_fees(trade) freqtrade.handle_insufficient_funds(trade)
assert log_has_re(f"Trying to reupdate {enter_side(is_short)} fees for " # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
r".*", caplog)
assert mock_uts.call_count == 0 assert mock_uts.call_count == 0
assert not log_has_re(f"Updating {enter_side(is_short)}-fee on trade "
r".* for order .*\.", caplog)
@pytest.mark.usefixtures("init_persistence")
def test_handle_insufficient_funds(mocker, default_conf_usdt, fee):
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order')
mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees')
create_mock_trades(fee, is_short=False)
trades = Trade.get_trades().all()
# Trade 0 has only a open buy order, no closed order
freqtrade.handle_insufficient_funds(trades[0])
assert mock_rlo.call_count == 0
assert mock_bof.call_count == 1
mock_rlo.reset_mock()
mock_bof.reset_mock()
# Trade 1 has closed buy and sell orders
freqtrade.handle_insufficient_funds(trades[1])
assert mock_rlo.call_count == 1
assert mock_bof.call_count == 0
mock_rlo.reset_mock()
mock_bof.reset_mock()
# Trade 2 has closed buy and sell orders
freqtrade.handle_insufficient_funds(trades[2])
assert mock_rlo.call_count == 1
assert mock_bof.call_count == 0
mock_rlo.reset_mock()
mock_bof.reset_mock()
# Trade 3 has an opne buy order
freqtrade.handle_insufficient_funds(trades[3])
assert mock_rlo.call_count == 0
assert mock_bof.call_count == 1
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog, is_short): def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
@ -4716,7 +4671,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog, is_short):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_1(is_short=is_short) order = mock_order_1(is_short=is_short)
assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog)
assert mock_fo.call_count == 0 assert mock_fo.call_count == 0
@ -4734,13 +4689,13 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog, is_short):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_4(is_short=is_short) order = mock_order_4(is_short=is_short)
assert log_has_re(r"Trying to refind Order\(.*", caplog) assert log_has_re(r"Trying to refind Order\(.*", caplog)
assert mock_fo.call_count == 0 assert mock_fo.call_count == 1
assert mock_uts.call_count == 0 assert mock_uts.call_count == 1
# No change to orderid - as update_trade_state is mocked # Found open buy order
assert trade.open_order_id is None assert trade.open_order_id is not None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
caplog.clear() caplog.clear()
@ -4752,11 +4707,11 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog, is_short):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_5_stoploss(is_short=is_short) order = mock_order_5_stoploss(is_short=is_short)
assert log_has_re(r"Trying to refind Order\(.*", caplog) assert log_has_re(r"Trying to refind Order\(.*", caplog)
assert mock_fo.call_count == 1 assert mock_fo.call_count == 1
assert mock_uts.call_count == 1 assert mock_uts.call_count == 2
# stoploss_order_id is "refound" and added to the trade # stoploss_order_id is "refound" and added to the trade
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is not None assert trade.stoploss_order_id is not None
@ -4771,7 +4726,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog, is_short):
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
freqtrade.refind_lost_order(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_6_sell(is_short=is_short) order = mock_order_6_sell(is_short=is_short)
assert log_has_re(r"Trying to refind Order\(.*", caplog) assert log_has_re(r"Trying to refind Order\(.*", caplog)
assert mock_fo.call_count == 1 assert mock_fo.call_count == 1
@ -4787,7 +4742,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog, is_short):
side_effect=ExchangeError()) side_effect=ExchangeError())
order = mock_order_5_stoploss(is_short=is_short) order = mock_order_5_stoploss(is_short=is_short)
freqtrade.refind_lost_order(trades[4]) freqtrade.handle_insufficient_funds(trades[4])
assert log_has(f"Error updating {order['id']}.", caplog) assert log_has(f"Error updating {order['id']}.", caplog)

View File

@ -21,16 +21,19 @@ def test_decimals_per_coin():
def test_round_coin_value(): def test_round_coin_value():
assert round_coin_value(222.222222, 'USDT') == '222.222 USDT' assert round_coin_value(222.222222, 'USDT') == '222.222 USDT'
assert round_coin_value(222.2, 'USDT') == '222.200 USDT' assert round_coin_value(222.2, 'USDT', keep_trailing_zeros=True) == '222.200 USDT'
assert round_coin_value(222.2, 'USDT') == '222.2 USDT'
assert round_coin_value(222.12745, 'EUR') == '222.127 EUR' assert round_coin_value(222.12745, 'EUR') == '222.127 EUR'
assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC' assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC'
assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH' assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH'
assert round_coin_value(222.222222, 'USDT', False) == '222.222' assert round_coin_value(222.222222, 'USDT', False) == '222.222'
assert round_coin_value(222.2, 'USDT', False) == '222.200' assert round_coin_value(222.2, 'USDT', False) == '222.2'
assert round_coin_value(222.00, 'USDT', False) == '222'
assert round_coin_value(222.12745, 'EUR', False) == '222.127' assert round_coin_value(222.12745, 'EUR', False) == '222.127'
assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121' assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121'
assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745' assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745'
assert round_coin_value(222.2, 'USDT', False, True) == '222.200'
def test_shorten_date() -> None: def test_shorten_date() -> None:

View File

@ -38,13 +38,17 @@ def test_init_custom_db_url(default_conf, tmpdir):
init_db(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert Path(filename).is_file() assert Path(filename).is_file()
r = Trade._session.execute(text("PRAGMA journal_mode"))
assert r.first() == ('wal',)
def test_init_invalid_db_url(default_conf): def test_init_invalid_db_url():
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
default_conf.update({'db_url': 'unknown:///some.url'})
with pytest.raises(OperationalException, match=r'.*no valid database URL*'): with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init_db(default_conf['db_url'], default_conf['dry_run']) init_db('unknown:///some.url', True)
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
init_db('sqlite:///', True)
def test_init_prod_db(default_conf, mocker): def test_init_prod_db(default_conf, mocker):
@ -2080,11 +2084,14 @@ def test_select_order(fee, is_short):
order = trades[4].select_order(trades[4].enter_side, False) order = trades[4].select_order(trades[4].enter_side, False)
assert order is not None assert order is not None
trades[4].orders[1].ft_order_side = trades[4].exit_side
order = trades[4].select_order(trades[4].exit_side, True) order = trades[4].select_order(trades[4].exit_side, True)
assert order is not None assert order is not None
trades[4].orders[1].ft_order_side = 'stoploss'
order = trades[4].select_order('stoploss', None)
assert order is not None
assert order.ft_order_side == 'stoploss' assert order.ft_order_side == 'stoploss'
order = trades[4].select_order(trades[4].exit_side, False)
assert order is None
def test_Trade_object_idem(): def test_Trade_object_idem():