Merge branch 'freqtrade:develop' into plot_hyperopt_stats
This commit is contained in:
commit
8d9d003671
@ -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,
|
||||||
|
BIN
docs/assets/windows_install.png
Normal file
BIN
docs/assets/windows_install.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = 'develop'
|
__version__ = '2022.1'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if __version__ == 'develop':
|
||||||
|
|
||||||
|
@ -106,15 +106,18 @@ 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)
|
||||||
|
self._log_exchange_response('fetch_stoploss_order1', order1)
|
||||||
|
# Fake type to stop - as this was really a stop order.
|
||||||
|
order1['id_stop'] = order1['id']
|
||||||
|
order1['id'] = order_id
|
||||||
|
order1['type'] = 'stop'
|
||||||
|
order1['status_stop'] = 'triggered'
|
||||||
|
return order1
|
||||||
|
|
||||||
order1 = self._api.fetch_order(real_order_id, pair)
|
|
||||||
self._log_exchange_response('fetch_stoploss_order1', order1)
|
|
||||||
# Fake type to stop - as this was really a stop order.
|
|
||||||
order1['id_stop'] = order1['id']
|
|
||||||
order1['id'] = order_id
|
|
||||||
order1['type'] = 'stop'
|
|
||||||
order1['status_stop'] = 'triggered'
|
|
||||||
return order1
|
|
||||||
return order[0]
|
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}")
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
try:
|
||||||
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
# call the custom exit price,with default value as previous closerate
|
# 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']
|
||||||
|
@ -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)),
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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')
|
||||||
|
@ -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,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -1423,7 +1428,7 @@ def test_get_strategy_run_id(default_conf_usdt):
|
|||||||
default_conf_usdt.update({
|
default_conf_usdt.update({
|
||||||
'strategy': 'StrategyTestV2',
|
'strategy': 'StrategyTestV2',
|
||||||
'max_open_trades': float('inf')
|
'max_open_trades': float('inf')
|
||||||
})
|
})
|
||||||
strategy = StrategyResolver.load_strategy(default_conf_usdt)
|
strategy = StrategyResolver.load_strategy(default_conf_usdt)
|
||||||
x = get_strategy_run_id(strategy)
|
x = get_strategy_run_id(strategy)
|
||||||
assert isinstance(x, str)
|
assert isinstance(x, str)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user