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
commit 8d9d003671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 488 additions and 217 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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

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

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

View File

@ -54,6 +54,8 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first. You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
![Windows installation](assets/windows_install.png)
--- ---

View File

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

View File

@ -106,7 +106,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.
@ -115,6 +117,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

@ -979,10 +979,10 @@ class FreqtradeBot(LoggingMixin):
or (order_obj and self.strategy.ft_check_timed_out( or (order_obj and self.strategy.ft_check_timed_out(
'sell', trade, order_obj, datetime.now(timezone.utc)) '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() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) 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 ' logger.warning(f'Emergencyselling trade {trade}, as the sell order '
f'timed out {max_timeouts} times.') f'timed out {max_timeouts} times.')
try: try:
@ -1021,12 +1021,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 unsellable trade.") f"as the filled amount of {filled_val} would result in an unsellable trade.")
@ -1079,11 +1079,12 @@ class FreqtradeBot(LoggingMixin):
reason=reason) reason=reason)
return was_trade_fully_canceled 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 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 trade is not partially completed, just cancel the order
if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
if not self.exchange.check_order_canceled_empty(order): if not self.exchange.check_order_canceled_empty(order):
@ -1094,7 +1095,7 @@ class FreqtradeBot(LoggingMixin):
trade.update_order(co) trade.update_order(co)
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}") 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) logger.info('Sell order %s for %s.', reason, trade)
else: else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
@ -1108,9 +1109,11 @@ class FreqtradeBot(LoggingMixin):
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
cancelled = True
else: else:
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
cancelled = False
self.wallets.update() self.wallets.update()
self._notify_exit_cancel( self._notify_exit_cancel(
@ -1118,7 +1121,7 @@ class FreqtradeBot(LoggingMixin):
order_type=self.strategy.order_types['sell'], order_type=self.strategy.order_types['sell'],
reason=reason reason=reason
) )
return reason return cancelled
def _safe_exit_amount(self, pair: str, amount: float) -> float: 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. # Handling of this will happen in check_handle_timedout.
return True 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.recalc_trade_from_orders()
Trade.commit() Trade.commit()
@ -1411,17 +1419,15 @@ class FreqtradeBot(LoggingMixin):
return real_amount return real_amount
return 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 update amount (binance-fix)
try: try:
new_amount = self.get_real_amount(trade, order) new_amount = self.get_real_amount(trade, order)
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
abs_tol=constants.MATH_CLOSE_PREC): abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount order_obj.ft_fee_base = trade.amount - new_amount
order.pop('filled', None)
except DependencyException as exception: except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception) logger.warning("Could not update trade amount: %s", exception)
return order
def get_real_amount(self, trade: Trade, order: Dict) -> float: 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) 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

@ -128,7 +128,8 @@ class Backtesting:
def __del__(self): def __del__(self):
self.cleanup() self.cleanup()
def cleanup(self): @staticmethod
def cleanup():
LoggingMixin.show_output = True LoggingMixin.show_output = True
PairLocks.use_db = True PairLocks.use_db = True
Trade.use_db = True Trade.use_db = True
@ -357,6 +358,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.
@ -414,7 +427,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

@ -57,7 +57,7 @@ def set_sequence_ids(engine, order_id, trade_id):
def migrate_trades_and_orders_table( def migrate_trades_and_orders_table(
decl_base, inspector, engine, decl_base, inspector, engine,
trade_back_name: str, cols: List, 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 = get_column_def(cols, 'fee_open', 'fee')
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
fee_open_currency = get_column_def(cols, 'fee_open_currency', '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} 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) 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")) 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 # let SQLAlchemy create the schema as required
with engine.begin() as connection: with engine.begin() as connection:
connection.execute(text(f""" connection.execute(text(f"""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, 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, 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, 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, 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} 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: 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
@ -193,6 +202,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols = inspector.get_columns('trades')
cols_orders = inspector.get_columns('orders')
tabs = get_table_names_for_table(inspector, 'trades') tabs = get_table_names_for_table(inspector, 'trades')
table_back_name = get_backup_name(tabs, 'trades_bak') table_back_name = get_backup_name(tabs, 'trades_bak')
order_tabs = get_table_names_for_table(inspector, 'orders') 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 # Check if migration necessary
# Migrates both trades and orders table! # 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 - " logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}") f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table( migrate_trades_and_orders_table(
decl_base, inspector, engine, table_back_name, cols, order_table_bak_name) decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders)
# Reread columns - the above recreated the table!
inspector = inspect(engine)
cols = inspector.get_columns('trades')
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

@ -16,7 +16,6 @@ from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.migrations import check_migrate
@ -39,6 +38,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,
@ -113,14 +115,15 @@ class Order(_DECL_BASE):
trade = relationship("Trade", back_populates="orders") trade = relationship("Trade", back_populates="orders")
ft_order_side = Column(String(25), nullable=False) # order_side can only be 'buy', 'sell' or 'stoploss'
ft_pair = Column(String(25), nullable=False) 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) ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
order_id = Column(String(255), nullable=False, index=True) order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True) status = Column(String(255), nullable=True)
symbol = Column(String(25), 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) side = Column(String(25), nullable=True)
price = Column(Float, nullable=True) price = Column(Float, nullable=True)
average = 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_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True)
ft_fee_base = Column(Float, nullable=True)
@property @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) 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): def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' 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"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") 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. Updates this entity with amount and actual open/close rates.
:param order: order retrieved by exchange.fetch_order() :param order: order retrieved by exchange.fetch_order()
:return: None :return: None
""" """
order_type = order['type']
# Ignore open and cancelled orders # 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 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 # Update open rate and actual amount
self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.open_rate = order.safe_price
self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.amount = order.safe_amount_after_fee
if self.is_open: 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.open_order_id = None
self.recalc_trade_from_orders() 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: if self.is_open:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) self.close(order.safe_price)
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): elif order.ft_order_side == 'stoploss':
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()} is hit for {self}.') logger.info(f'{order.order_type.upper()} is hit for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) self.close(order.safe_price)
else: else:
raise ValueError(f'Unknown order type: {order_type}') raise ValueError(f'Unknown order type: {order.order_type}')
Trade.commit() Trade.commit()
def close(self, rate: float, *, show_msg: bool = True) -> None: def close(self, rate: float, *, show_msg: bool = True) -> None:
@ -628,7 +649,7 @@ class LocalTrade():
(o.status not in NON_OPEN_EXCHANGE_STATES)): (o.status not in NON_OPEN_EXCHANGE_STATES)):
continue continue
tmp_amount = o.amount tmp_amount = o.safe_amount_after_fee
tmp_price = o.average or o.price tmp_price = o.average or o.price
if o.filled is not None: if o.filled is not None:
tmp_amount = o.filled tmp_amount = o.filled
@ -799,11 +820,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

@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
from freqtrade.enums import BacktestState from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse 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.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -20,8 +20,9 @@ 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), ws_mode=Depends(is_webserver_mode)):
"""Start backtesting if not done so already""" """Start backtesting if not done so already"""
if ApiServer._bgtask_running: if ApiServer._bgtask_running:
raise RPCException('Bot Background task already 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']) @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. Get backtesting result.
Returns Result after backtesting has been ran. Returns Result after backtesting has been ran.
@ -156,7 +157,7 @@ def api_get_backtest():
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', '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""" """Reset backtesting"""
if ApiServer._bgtask_running: if ApiServer._bgtask_running:
return { return {
@ -182,7 +183,7 @@ def api_delete_backtest():
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', '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: if not ApiServer._bgtask_running:
return { return {
"status": "not_running", "status": "not_running",

View File

@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional
from fastapi import Depends from fastapi import Depends
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)):
ApiServer._exchange = ExchangeResolver.load_exchange( ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config) config['exchange']['name'], config)
return ApiServer._exchange 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, '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

View File

@ -790,12 +790,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

View File

@ -22,7 +22,7 @@ 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.9 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,7 +5,7 @@
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
matplotlib matplotlib

View File

@ -2,7 +2,7 @@ numpy==1.22.2
pandas==1.4.1 pandas==1.4.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.72.98 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,14 +25,14 @@ 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.5
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0
psutil==5.9.0 psutil==5.9.0

View File

@ -201,6 +201,9 @@ def create_mock_trades(fee, use_db: bool = True):
""" """
Create some fake trades ... Create some fake trades ...
""" """
if use_db:
Trade.query.session.rollback()
def add_trade(trade): def add_trade(trade):
if use_db: if use_db:
Trade.query.session.add(trade) Trade.query.session.add(trade)
@ -1221,7 +1224,7 @@ def limit_sell_order_open():
'id': 'mocked_limit_sell', 'id': 'mocked_limit_sell',
'type': 'limit', 'type': 'limit',
'side': 'sell', 'side': 'sell',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp, 'timestamp': arrow.utcnow().int_timestamp,
'price': 0.00001173, 'price': 0.00001173,
@ -2208,7 +2211,7 @@ def limit_sell_order_usdt_open():
'id': 'mocked_limit_sell_usdt', 'id': 'mocked_limit_sell_usdt',
'type': 'limit', 'type': 'limit',
'side': 'sell', 'side': 'sell',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'timestamp': arrow.utcnow().int_timestamp, 'timestamp': arrow.utcnow().int_timestamp,
'price': 2.20, 'price': 2.20,

View File

@ -125,7 +125,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf):
assert not exchange.stoploss_adjust(1501, order) assert not exchange.stoploss_adjust(1501, order)
def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order):
default_conf['dry_run'] = True default_conf['dry_run'] = True
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
@ -147,9 +147,15 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
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
@ -158,6 +164,17 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
assert resp['type'] == 'stop' assert resp['type'] == 'stop'
assert resp['status_stop'] == 'triggered' assert resp['status_stop'] == 'triggered'
# 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')
assert resp
# fetch_order not called (no regular order ID)
assert api_mock.fetch_order.call_count == 0
assert order == order
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"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')

View File

@ -562,9 +562,9 @@ 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],
@ -574,13 +574,27 @@ tc36 = BTContainer(data=[
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, 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=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,
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],
@ -649,6 +663,7 @@ TESTS = [
tc36, tc36,
tc37, tc37,
tc38, tc38,
tc39,
] ]

View File

@ -52,6 +52,13 @@ def trim_dictlist(dict_list, num):
return new return new
@pytest.fixture(autouse=True)
def backtesting_cleanup() -> None:
yield None
Backtesting.cleanup()
def load_data_test(what, testdatadir): def load_data_test(what, testdatadir):
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
data = history.load_pair_history(pair='UNITTEST/BTC', datadir=testdatadir, data = history.load_pair_history(pair='UNITTEST/BTC', datadir=testdatadir,
@ -553,8 +560,6 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None
backtesting.cleanup()
def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
default_conf['use_sell_signal'] = False default_conf['use_sell_signal'] = False

View File

@ -11,6 +11,7 @@ from freqtrade.edge import PairInfo
from freqtrade.enums import State from freqtrade.enums import State
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import Order
from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@ -277,8 +278,10 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
assert trade assert trade
# Simulate buy & sell # Simulate buy & sell
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update(limit_sell_order) trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -415,28 +418,32 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell')
trade.update_trade(oobj)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -495,14 +502,16 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up, fetch_ticker=ticker_sell_up,
get_fee=fee get_fee=fee
) )
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -754,13 +763,13 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.fetch_order', 'freqtrade.exchange.Exchange.fetch_order',
side_effect=[{ side_effect=[{
'id': '1234', 'id': trade.orders[0].order_id,
'status': 'open', 'status': 'open',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'filled': filled_amount 'filled': filled_amount
}, { }, {
'id': '1234', 'id': trade.orders[0].order_id,
'status': 'closed', 'status': 'closed',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
@ -840,10 +849,12 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -874,10 +885,12 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -946,10 +959,12 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1018,10 +1033,12 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False

View File

@ -1108,6 +1108,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
data='{"tradeid": "1"}') data='{"tradeid": "1"}')
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"} assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"}
Trade.query.session.rollback()
ftbot.enter_positions() ftbot.enter_positions()
@ -1349,6 +1350,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
ftbot, client = botclient ftbot, client = botclient
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
rc = client_get(client, f"{BASE_URI}/backtest")
# Backtest prevented in default mode
assert_response(rc, 502)
ftbot.config['runmode'] = RunMode.WEBSERVER
# Backtesting not started yet # Backtesting not started yet
rc = client_get(client, f"{BASE_URI}/backtest") rc = client_get(client, f"{BASE_URI}/backtest")
assert_response(rc) assert_response(rc)

View File

@ -418,10 +418,12 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -461,8 +463,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
trade.update(limit_buy_order) trade.update_trade(oobj)
trade.update(limit_sell_order) trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -527,10 +529,12 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -574,8 +578,8 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
trade.update(limit_buy_order) trade.update_trade(oobj)
trade.update(limit_sell_order) trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -643,10 +647,12 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -690,8 +696,8 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
trade.update(limit_buy_order) trade.update_trade(oobj)
trade.update(limit_sell_order) trade.update_trade(oobjs)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -761,7 +767,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
context = MagicMock() context = MagicMock()
# Test with invalid 2nd argument (should silently pass) # Test with invalid 2nd argument (should silently pass)
context.args = ["aaa"] context.args = ["aaa"]
@ -770,13 +778,15 @@ 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()
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
trade.update(limit_sell_order) # Simulate fulfilled LIMIT_SELL order for trade
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.now(timezone.utc) trade.close_date = datetime.now(timezone.utc)
trade.is_open = False trade.is_open = False
@ -845,7 +855,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
@ -861,7 +871,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:
@ -874,7 +884,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:
@ -1286,10 +1296,12 @@ def test_telegram_performance_handle(default_conf, update, ticker, fee,
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1313,13 +1325,15 @@ def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
trade.buy_tag = "TESTBUY"
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
trade.buy_tag = "TESTBUY"
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1356,13 +1370,14 @@ def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, f
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
trade.sell_reason = 'TESTSELL' trade.sell_reason = 'TESTSELL'
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1399,15 +1414,16 @@ def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
freqtradebot.enter_positions() freqtradebot.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
trade.buy_tag = "TESTBUY" trade.buy_tag = "TESTBUY"
trade.sell_reason = "TESTSELL" trade.sell_reason = "TESTSELL"
# Simulate fulfilled LIMIT_BUY order for trade
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
trade.update_trade(oobj)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
trade.update_trade(oobj)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
@ -1734,7 +1750,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'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',
@ -1751,7 +1767,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
'*Amount:* `1333.33333333`\n' \ '*Amount:* `1333.33333333`\n' \
'*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'}
caplog.clear() caplog.clear()
@ -1825,7 +1841,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
'buy_tag': 'buy_signal_01', 'buy_tag': 'buy_signal_01',
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'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',
@ -1839,7 +1855,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
'*Buy Tag:* `buy_signal_01`\n' \ '*Buy Tag:* `buy_signal_01`\n' \
'*Amount:* `1333.33333333`\n' \ '*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \ '*Open Rate:* `0.00001099`\n' \
'*Total:* `(0.00100000 BTC, 12.345 USD)`' '*Total:* `(0.01465333 BTC, 180.895 USD)`'
def test_send_msg_sell_notification(default_conf, mocker) -> None: def test_send_msg_sell_notification(default_conf, mocker) -> None:
@ -2031,7 +2047,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'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,
@ -2044,7 +2060,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*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)`')
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:

View File

@ -6,7 +6,7 @@ import time
from copy import deepcopy from copy import deepcopy
from math import isclose from math import isclose
from typing import List from typing import List
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock, patch
import arrow import arrow
import pytest import pytest
@ -227,7 +227,8 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker,
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
############################################# #############################################
# stoploss shoud be hit # stoploss shoud be hit
@ -292,7 +293,8 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, fee,
assert trade.exchange == 'binance' assert trade.exchange == 'binance'
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert trade.amount == 30.0 assert trade.amount == 30.0
@ -982,11 +984,17 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog,
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
trade.stoploss_order_id = 100 trade.stoploss_order_id = "100"
trade.orders.append(Order(
ft_order_side='stoploss',
order_id='100',
ft_pair=trade.pair,
ft_is_open=True,
))
assert trade assert trade
stoploss_order_hit = MagicMock(return_value={ stoploss_order_hit = MagicMock(return_value={
'id': 100, 'id': "100",
'status': 'closed', 'status': 'closed',
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
'price': 3, 'price': 3,
@ -1632,9 +1640,9 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_buy_order_usdt['amount']) return_value=limit_buy_order_usdt['amount'])
order_id = limit_buy_order_usdt['id']
trade = Trade( trade = Trade(
open_order_id=123, open_order_id=order_id,
fee_open=0.001, fee_open=0.001,
fee_close=0.001, fee_close=0.001,
open_rate=0.01, open_rate=0.01,
@ -1642,29 +1650,35 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
amount=11, amount=11,
exchange="binance", exchange="binance",
) )
trade.orders.append(Order(
ft_order_side='buy',
price=0.01,
order_id=order_id,
))
assert not freqtrade.update_trade_state(trade, None) assert not freqtrade.update_trade_state(trade, None)
assert log_has_re(r'Orderid for trade .* is empty.', caplog) assert log_has_re(r'Orderid for trade .* is empty.', caplog)
caplog.clear() caplog.clear()
# Add datetime explicitly since sqlalchemy defaults apply only once written to database # Add datetime explicitly since sqlalchemy defaults apply only once written to database
freqtrade.update_trade_state(trade, '123') freqtrade.update_trade_state(trade, order_id)
# Test amount not modified by fee-logic # Test amount not modified by fee-logic
assert not log_has_re(r'Applying fee to .*', caplog) assert not log_has_re(r'Applying fee to .*', caplog)
caplog.clear() caplog.clear()
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.amount == limit_buy_order_usdt['amount'] assert trade.amount == limit_buy_order_usdt['amount']
trade.open_order_id = '123' trade.open_order_id = order_id
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
assert trade.amount != 90.81 assert trade.amount != 90.81
# test amount modified by fee-logic # test amount modified by fee-logic
freqtrade.update_trade_state(trade, '123') freqtrade.update_trade_state(trade, order_id)
assert trade.amount == 90.81 assert trade.amount == 90.81
assert trade.open_order_id is None assert trade.open_order_id is None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
# Assert we call handle_trade() if trade is feasible for execution # Assert we call handle_trade() if trade is feasible for execution
freqtrade.update_trade_state(trade, '123') freqtrade.update_trade_state(trade, order_id)
assert log_has_re('Found open order for.*', caplog) assert log_has_re('Found open order for.*', caplog)
limit_buy_order_usdt_new = deepcopy(limit_buy_order_usdt) limit_buy_order_usdt_new = deepcopy(limit_buy_order_usdt)
@ -1673,7 +1687,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new)
res = freqtrade.update_trade_state(trade, '123') res = freqtrade.update_trade_state(trade, order_id)
# Cancelled empty # Cancelled empty
assert res is True assert res is True
@ -1685,6 +1699,8 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap
def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, limit_buy_order_usdt, def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, limit_buy_order_usdt,
fee, mocker, initial_amount, has_rounding_fee, caplog): fee, mocker, initial_amount, has_rounding_fee, caplog):
trades_for_order[0]['amount'] = initial_amount trades_for_order[0]['amount'] = initial_amount
order_id = "oid_123456"
limit_buy_order_usdt['id'] = order_id
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# fetch_order should not be called!! # fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
@ -1700,14 +1716,26 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l
open_date=arrow.utcnow().datetime, open_date=arrow.utcnow().datetime,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
open_order_id="123456", open_order_id=order_id,
is_open=True, is_open=True,
) )
freqtrade.update_trade_state(trade, '123456', limit_buy_order_usdt) trade.orders.append(
Order(
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=True,
order_id=order_id,
)
)
freqtrade.update_trade_state(trade, order_id, limit_buy_order_usdt)
assert trade.amount != amount assert trade.amount != amount
assert trade.amount == limit_buy_order_usdt['amount'] log_text = r'Applying fee on amount for .*'
if has_rounding_fee: if has_rounding_fee:
assert log_has_re(r'Applying fee on amount for .*', caplog) assert pytest.approx(trade.amount) == 29.992
assert log_has_re(log_text, caplog)
else:
assert pytest.approx(trade.amount) == limit_buy_order_usdt['amount']
assert not log_has_re(log_text, caplog)
def test_update_trade_state_exception(mocker, default_conf_usdt, def test_update_trade_state_exception(mocker, default_conf_usdt,
@ -1762,7 +1790,7 @@ def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell
fee_open=0.0025, fee_open=0.0025,
fee_close=0.0025, fee_close=0.0025,
open_date=arrow.utcnow().datetime, open_date=arrow.utcnow().datetime,
open_order_id="123456", open_order_id=limit_sell_order_usdt_open['id'],
is_open=True, is_open=True,
) )
order = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'LTC/ETH', 'sell') order = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'LTC/ETH', 'sell')
@ -1803,7 +1831,8 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_
assert trade assert trade
time.sleep(0.01) # Race condition fix time.sleep(0.01) # Race condition fix
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
assert trade.is_open is True assert trade.is_open is True
freqtrade.wallets.update() freqtrade.wallets.update()
@ -1812,7 +1841,9 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_
assert trade.open_order_id == limit_sell_order_usdt['id'] assert trade.open_order_id == limit_sell_order_usdt['id']
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
trade.update_trade(oobj)
assert trade.close_rate == 2.2 assert trade.close_rate == 2.2
assert trade.close_profit == 0.09451372 assert trade.close_profit == 0.09451372
@ -1962,8 +1993,11 @@ def test_close_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt,
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update(limit_sell_order_usdt) trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
trade.update_trade(oobj)
assert trade.is_open is False assert trade.is_open is False
with pytest.raises(DependencyException, match=r'.*closed trade.*'): with pytest.raises(DependencyException, match=r'.*closed trade.*'):
@ -1986,7 +2020,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog):
def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, limit_buy_order_old, def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, limit_buy_order_old,
open_trade, fee, mocker) -> None: open_trade, fee, mocker) -> None:
default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30} default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30}
limit_buy_order_old['id'] = open_trade.open_order_id
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order_old) cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
cancel_buy_order = deepcopy(limit_buy_order_old) cancel_buy_order = deepcopy(limit_buy_order_old)
@ -2186,9 +2220,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
caplog.clear() caplog.clear()
# 2nd canceled trade ... # 2nd canceled trade ...
open_trade.open_order_id = limit_sell_order_old['id'] open_trade.open_order_id = limit_sell_order_old['id']
# If cancelling fails - no emergency sell!
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
freqtrade.check_handle_timedout()
assert et_mock.call_count == 0
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert log_has_re('Emergencyselling trade.*', caplog) assert log_has_re('Emergencyselling trade.*', caplog)
assert et_mock.call_count == 1 assert et_mock.call_count == 1
@ -2289,6 +2328,7 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_
limit_buy_order_old_partial_canceled, mocker) -> None: limit_buy_order_old_partial_canceled, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
limit_buy_order_old_partial['id'] = open_trade.open_order_id limit_buy_order_old_partial['id'] = open_trade.open_order_id
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
patch_exchange(mocker) patch_exchange(mocker)
@ -2436,6 +2476,9 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_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, limit_buy_order_usdt, reason) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, 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_buy_order_usdt, reason)
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
@ -2526,13 +2569,17 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
send_msg_mock.reset_mock() send_msg_mock.reset_mock()
order['amount'] = 2 order['amount'] = 2
assert freqtrade.handle_cancel_exit(trade, order, reason assert not freqtrade.handle_cancel_exit(trade, order, reason)
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Assert cancel_order was not called (callcount remains unchanged) # Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
assert freqtrade.handle_cancel_exit(trade, order, reason assert (send_msg_mock.call_args_list[0][0][0]['reason']
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
assert not freqtrade.handle_cancel_exit(trade, order, reason)
send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Message should not be iterated again # Message should not be iterated again
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
@ -2551,7 +2598,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
order = {'remaining': 1, order = {'remaining': 1,
'amount': 1, 'amount': 1,
'status': "open"} 'status': "open"}
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' assert not freqtrade.handle_cancel_exit(trade, order, reason)
def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker
@ -3100,7 +3147,8 @@ def test_sell_profit_only(
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True, None, None)) patch_get_signal(freqtrade, value=(False, True, None, None))
assert freqtrade.handle_trade(trade) is handle_first assert freqtrade.handle_trade(trade) is handle_first
@ -3136,7 +3184,9 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_
trade = Trade.query.first() trade = Trade.query.first()
amnt = trade.amount amnt = trade.amount
trade.update(limit_buy_order_usdt)
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
patch_get_signal(freqtrade, value=(False, True, None, None)) patch_get_signal(freqtrade, value=(False, True, None, None))
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985))
@ -3244,7 +3294,8 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(True, True, None, None)) patch_get_signal(freqtrade, value=(True, True, None, None))
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -3347,7 +3398,8 @@ def test_trailing_stop_loss_positive(
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
# stop-loss not reached # stop-loss not reached
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -3434,7 +3486,8 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
# Sell due to min_roi_reached # Sell due to min_roi_reached
patch_get_signal(freqtrade, value=(True, False, None, None)) patch_get_signal(freqtrade, value=(True, False, None, None))
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
@ -3809,7 +3862,8 @@ def test_order_book_depth_of_market(
assert len(Trade.query.all()) == 1 assert len(Trade.query.all()) == 1
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert whitelist == default_conf_usdt['exchange']['pair_whitelist'] assert whitelist == default_conf_usdt['exchange']['pair_whitelist']
@ -3903,7 +3957,8 @@ def test_order_book_ask_strategy(
assert trade assert trade
time.sleep(0.01) # Race condition fix time.sleep(0.01) # Race condition fix
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy')
trade.update_trade(oobj)
freqtrade.wallets.update() freqtrade.wallets.update()
assert trade.is_open is True assert trade.is_open is True

View File

@ -4,6 +4,7 @@ import pytest
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import Order
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC
from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.interface import SellCheckTuple
from tests.conftest import get_patched_freqtradebot, patch_get_signal from tests.conftest import get_patched_freqtradebot, patch_get_signal
@ -94,7 +95,11 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
trades = Trade.query.all() trades = Trade.query.all()
# Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
for trade in trades: for trade in trades:
trade.stoploss_order_id = 3 stoploss_order_closed['id'] = '3'
oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss')
trade.orders.append(oobj)
trade.stoploss_order_id = '3'
trade.open_order_id = None trade.open_order_id = None
n = freqtrade.exit_positions(trades) n = freqtrade.exit_positions(trades)

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

@ -33,13 +33,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):
@ -108,7 +112,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca
assert trade.close_date is None assert trade.close_date is None
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 2.00 assert trade.open_rate == 2.00
assert trade.close_profit is None assert trade.close_profit is None
@ -119,7 +124,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca
caplog.clear() caplog.clear()
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_rate == 2.20 assert trade.close_rate == 2.20
assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_profit == round(0.0945137157107232, 8)
@ -146,7 +152,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(market_buy_order_usdt) oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 2.0 assert trade.open_rate == 2.0
assert trade.close_profit is None assert trade.close_profit is None
@ -158,7 +165,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
caplog.clear() caplog.clear()
trade.is_open = True trade.is_open = True
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(market_sell_order_usdt) oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_rate == 2.2 assert trade.close_rate == 2.2
assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_profit == round(0.0945137157107232, 8)
@ -181,9 +189,11 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade._calc_open_trade_value() == 60.15 assert trade._calc_open_trade_value() == 60.15
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert isclose(trade.calc_close_trade_value(), 65.835) assert isclose(trade.calc_close_trade_value(), 65.835)
# Profit in USDT # Profit in USDT
@ -236,7 +246,8 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.calc_close_trade_value() == 0.0 assert trade.calc_close_trade_value() == 0.0
@ -257,7 +268,8 @@ def test_update_open_order(limit_buy_order_usdt):
assert trade.close_date is None assert trade.close_date is None
limit_buy_order_usdt['status'] = 'open' limit_buy_order_usdt['status'] = 'open'
trade.update(limit_buy_order_usdt) oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_profit is None assert trade.close_profit is None
@ -276,8 +288,9 @@ def test_update_invalid_order(limit_buy_order_usdt):
exchange='binance', exchange='binance',
) )
limit_buy_order_usdt['type'] = 'invalid' limit_buy_order_usdt['type'] = 'invalid'
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'meep')
with pytest.raises(ValueError, match=r'Unknown order type'): with pytest.raises(ValueError, match=r'Unknown order type'):
trade.update(limit_buy_order_usdt) trade.update_trade(oobj)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -304,7 +317,8 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee):
exchange='binance', exchange='binance',
) )
trade.open_order_id = 'open_trade' trade.open_order_id = 'open_trade'
trade.update(limit_buy_order_usdt) # Buy @ 2.0 oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Get the open rate price with the standard fee rate # Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == 60.15 assert trade._calc_open_trade_value() == 60.15
@ -325,14 +339,16 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee
exchange='binance', exchange='binance',
) )
trade.open_order_id = 'close_trade' trade.open_order_id = 'close_trade'
trade.update(limit_buy_order_usdt) # Buy @ 2.0 oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Get the close rate price with a custom close rate and a regular fee rate # Get the close rate price with a custom close rate and a regular fee rate
assert trade.calc_close_trade_value(rate=2.5) == 74.8125 assert trade.calc_close_trade_value(rate=2.5) == 74.8125
# Get the close rate price with a custom close rate and a custom fee rate # Get the close rate price with a custom close rate and a custom fee rate
assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775 assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775
# Test when we apply a Sell order, and ask price with a custom fee rate # Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.calc_close_trade_value(fee=0.005) == 65.67 assert trade.calc_close_trade_value(fee=0.005) == 65.67
@ -409,7 +425,9 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee):
exchange='binance', exchange='binance',
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) # Buy @ 2.0 oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Custom closing rate and regular fee rate # Custom closing rate and regular fee rate
# Higher than open rate - 2.1 quote # Higher than open rate - 2.1 quote
@ -424,7 +442,8 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee):
assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8) assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8)
# Test when we apply a Sell order. Sell higher than open rate @ 2.2 # Test when we apply a Sell order. Sell higher than open rate @ 2.2
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.calc_profit() == round(5.684999999999995, 8) assert trade.calc_profit() == round(5.684999999999995, 8)
# Test with a custom fee rate on the close trade # Test with a custom fee rate on the close trade
@ -443,7 +462,9 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee):
exchange='binance' exchange='binance'
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) # Buy @ 2.0
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
trade.update_trade(oobj) # Buy @ 2.0
# Higher than open rate - 2.1 quote # Higher than open rate - 2.1 quote
assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8) assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8)
@ -457,7 +478,8 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee):
assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8) assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8)
# Test when we apply a Sell order. Sell higher than open rate @ 2.2 # Test when we apply a Sell order. Sell higher than open rate @ 2.2
trade.update(limit_sell_order_usdt) oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell')
trade.update_trade(oobj)
assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) assert trade.calc_profit_ratio() == round(0.0945137157107232, 8)
# Test with a custom fee rate on the close trade # Test with a custom fee rate on the close trade