Merge pull request #5556 from samgermain/feat/short
Merged feat/short with develop
This commit is contained in:
commit
efd6c037d1
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -2,14 +2,16 @@ Thank you for sending your pull request. But first, have you included
|
||||
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Explain in one sentence the goal of this PR
|
||||
|
||||
Solve the issue: #___
|
||||
|
||||
## Quick changelog
|
||||
|
||||
- <change log #1>
|
||||
- <change log #2>
|
||||
- <change log 1>
|
||||
- <change log 1>
|
||||
|
||||
## What's new?
|
||||
|
||||
*Explain in details what this PR solve or improve. You can include visuals.*
|
||||
|
@ -13,7 +13,7 @@ RUN mkdir /freqtrade \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \
|
||||
&& apt-get clean \
|
||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
||||
&& useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
||||
|
@ -12,9 +12,12 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||
&& make -j$(nproc) \
|
||||
&& which sudo && sudo make install || make install \
|
||||
&& cd ..
|
||||
&& which sudo && sudo make install || make install
|
||||
if [ -x "$(command -v apt-get)" ]; then
|
||||
echo "Updating library path using ldconfig"
|
||||
sudo ldconfig
|
||||
fi
|
||||
cd .. && rm -rf ./ta-lib/
|
||||
else
|
||||
echo "TA-lib already installed, skipping installation"
|
||||
fi
|
||||
# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||
|
@ -80,7 +80,7 @@ To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_sp
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
# Define a custom stoploss space.
|
||||
def stoploss_space(self):
|
||||
def stoploss_space():
|
||||
return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')]
|
||||
```
|
||||
|
||||
|
@ -444,8 +444,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
This is ongoing work. For now, it is supported only for binance.
|
||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values.
|
||||
This is ongoing work. For now, it is supported only for binance and kucoin.
|
||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||
|
||||
### Exchange configuration
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
||||
|
||||
!!! Warning
|
||||
WHen using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
||||
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
||||
|
||||
!!! Note
|
||||
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
||||
|
@ -4,6 +4,8 @@ This page combines common gotchas and informations which are exchange-specific a
|
||||
|
||||
## Binance
|
||||
|
||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
|
||||
@ -113,8 +115,12 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "your_exchange_api_key_password",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
### Kucoin Blacklists
|
||||
|
||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||
@ -158,6 +164,8 @@ For example, to test the order type `FOK` with Kraken, and modify candle limit t
|
||||
"order_time_in_force": ["gtc", "fok"],
|
||||
"ohlcv_candle_limit": 200
|
||||
}
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
|
@ -1,4 +1,4 @@
|
||||
mkdocs==1.2.2
|
||||
mkdocs-material==7.2.5
|
||||
mkdocs-material==7.2.6
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==8.2
|
||||
|
@ -22,7 +22,7 @@ if __version__ == 'develop':
|
||||
# subprocess.check_output(
|
||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
except Exception:
|
||||
except Exception: # pragma: no cover
|
||||
# git not available, ignore
|
||||
try:
|
||||
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
||||
|
@ -61,13 +61,13 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "text",
|
||||
"name": "stake_currency",
|
||||
"message": "Please insert your stake currency:",
|
||||
"default": 'BTC',
|
||||
"default": 'USDT',
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_amount",
|
||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"default": "0.01",
|
||||
"default": "100",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||
if val == UNLIMITED_STAKE_AMOUNT
|
||||
@ -105,6 +105,8 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"bittrex",
|
||||
"kraken",
|
||||
"ftx",
|
||||
"kucoin",
|
||||
"gateio",
|
||||
Separator(),
|
||||
"other",
|
||||
],
|
||||
@ -128,6 +130,12 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"message": "Insert Exchange Secret",
|
||||
"when": lambda x: not x['dry_run']
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "telegram",
|
||||
|
@ -18,6 +18,7 @@ class Binance(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
|
@ -54,12 +54,16 @@ class Exchange:
|
||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||
_params: Dict = {}
|
||||
|
||||
# Additional headers - added to the ccxt object
|
||||
_headers: Dict = {}
|
||||
|
||||
# Dict to specify which options each exchange implements
|
||||
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
||||
# or by specifying them in the configuration.
|
||||
_ft_has_default: Dict = {
|
||||
"stoploss_on_exchange": False,
|
||||
"order_time_in_force": ["gtc"],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
"ohlcv_partial_candle": True,
|
||||
@ -169,7 +173,7 @@ class Exchange:
|
||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||
|
||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
||||
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
|
||||
"""
|
||||
Initialize ccxt with given config and return valid
|
||||
ccxt instance.
|
||||
@ -188,6 +192,10 @@ class Exchange:
|
||||
}
|
||||
if ccxt_kwargs:
|
||||
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
||||
if self._headers:
|
||||
# Inject static headers after the above output to not confuse users.
|
||||
ccxt_kwargs = deep_merge_dicts({'headers': self._headers}, ccxt_kwargs)
|
||||
if ccxt_kwargs:
|
||||
ex_config.update(ccxt_kwargs)
|
||||
try:
|
||||
|
||||
@ -716,7 +724,8 @@ class Exchange:
|
||||
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'gtc' and ordertype != 'market':
|
||||
params.update({'timeInForce': time_in_force})
|
||||
param = self._ft_has.get('time_in_force_parameter', '')
|
||||
params.update({param: time_in_force})
|
||||
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
|
@ -21,4 +21,6 @@ class Kucoin(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"l2_limit_range": [20, 100],
|
||||
"l2_limit_range_required": False,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||
|
||||
# Protect sell-logic from forcesell and vice versa
|
||||
self._sell_lock = Lock()
|
||||
self._exit_lock = Lock()
|
||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
@ -166,14 +166,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
self.strategy.analyze(self.active_pair_whitelist)
|
||||
|
||||
with self._sell_lock:
|
||||
with self._exit_lock:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
|
||||
# Protect from collisions with forcesell.
|
||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||
# while selling is in process, since telegram messages arrive in an different thread.
|
||||
with self._sell_lock:
|
||||
with self._exit_lock:
|
||||
trades = Trade.get_open_trades()
|
||||
# First process current opened trades (positions)
|
||||
self.exit_positions(trades)
|
||||
@ -296,9 +296,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
if sell_order:
|
||||
self.refind_lost_order(trade)
|
||||
else:
|
||||
self.reupdate_buy_order_fees(trade)
|
||||
self.reupdate_enter_order_fees(trade)
|
||||
|
||||
def reupdate_buy_order_fees(self, trade: Trade):
|
||||
def reupdate_enter_order_fees(self, trade: Trade):
|
||||
"""
|
||||
Get buy order from database, and try to reupdate.
|
||||
Handles trades where the initial fee-update did not work.
|
||||
@ -476,21 +476,21 @@ class FreqtradeBot(LoggingMixin):
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
|
||||
if price:
|
||||
buy_limit_requested = price
|
||||
enter_limit_requested = price
|
||||
else:
|
||||
# Calculate price
|
||||
proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=proposed_buy_rate)(
|
||||
default_retval=proposed_enter_rate)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
proposed_rate=proposed_buy_rate)
|
||||
proposed_rate=proposed_enter_rate)
|
||||
|
||||
buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate)
|
||||
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||
|
||||
if not buy_limit_requested:
|
||||
if not enter_limit_requested:
|
||||
raise PricingError('Could not determine buy price.')
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
|
||||
self.strategy.stoploss)
|
||||
|
||||
if not self.edge:
|
||||
@ -498,7 +498,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
current_rate=buy_limit_requested, proposed_stake=stake_amount,
|
||||
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
@ -508,27 +508,27 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
|
||||
amount = stake_amount / buy_limit_requested
|
||||
amount = stake_amount / enter_limit_requested
|
||||
order_type = self.strategy.order_types['buy']
|
||||
if forcebuy:
|
||||
# Forcebuy can define a different ordertype
|
||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
|
||||
logger.info(f"User requested abortion of buying {pair}")
|
||||
return False
|
||||
amount = self.exchange.amount_to_precision(pair, amount)
|
||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||
amount=amount, rate=buy_limit_requested,
|
||||
amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
||||
order_id = order['id']
|
||||
order_status = order.get('status', None)
|
||||
|
||||
# we assume the order is executed at the price requested
|
||||
buy_limit_filled_price = buy_limit_requested
|
||||
enter_limit_filled_price = enter_limit_requested
|
||||
amount_requested = amount
|
||||
|
||||
if order_status == 'expired' or order_status == 'rejected':
|
||||
@ -551,13 +551,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
stake_amount = order['cost']
|
||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
|
||||
# in case of FOK the order may be filled immediately and fully
|
||||
elif order_status == 'closed':
|
||||
stake_amount = order['cost']
|
||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||
@ -569,8 +569,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount_requested=amount_requested,
|
||||
fee_open=fee,
|
||||
fee_close=fee,
|
||||
open_rate=buy_limit_filled_price,
|
||||
open_rate_requested=buy_limit_requested,
|
||||
open_rate=enter_limit_filled_price,
|
||||
open_rate_requested=enter_limit_requested,
|
||||
open_date=datetime.utcnow(),
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id,
|
||||
@ -590,11 +590,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Updating wallets
|
||||
self.wallets.update()
|
||||
|
||||
self._notify_buy(trade, order_type)
|
||||
self._notify_enter(trade, order_type)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
||||
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy occurred.
|
||||
"""
|
||||
@ -617,7 +617,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy cancel occurred.
|
||||
"""
|
||||
@ -643,7 +643,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_buy_fill(self, trade: Trade) -> None:
|
||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_FILL,
|
||||
@ -713,8 +713,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
|
||||
logger.debug('checking sell')
|
||||
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||
if self._check_and_execute_exit(trade, exit_rate, buy, sell):
|
||||
return True
|
||||
|
||||
logger.debug('Found no sell signal for %s.', trade)
|
||||
@ -744,7 +744,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
except InvalidOrderException as e:
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Selling the trade forcefully')
|
||||
logger.warning('Exiting the trade forcefully')
|
||||
self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||
sell_type=SellType.EMERGENCY_SELL))
|
||||
|
||||
@ -782,7 +782,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
self._notify_sell(trade, "stoploss")
|
||||
self._notify_exit(trade, "stoploss")
|
||||
return True
|
||||
|
||||
if trade.open_order_id or not trade.is_open:
|
||||
@ -851,19 +851,19 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||
buy: bool, sell: bool) -> bool:
|
||||
"""
|
||||
Check and execute sell
|
||||
Check and execute exit
|
||||
"""
|
||||
should_sell = self.strategy.should_sell(
|
||||
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
||||
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||
self.execute_trade_exit(trade, sell_rate, should_sell)
|
||||
self.execute_trade_exit(trade, exit_rate, should_sell)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -906,7 +906,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
@ -915,7 +915,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
def cancel_all_open_orders(self) -> None:
|
||||
"""
|
||||
@ -931,13 +931,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
continue
|
||||
|
||||
if order['side'] == 'buy':
|
||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
|
||||
elif order['side'] == 'sell':
|
||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
Trade.commit()
|
||||
|
||||
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||
def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||
"""
|
||||
Buy cancel - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
@ -994,11 +994,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||
reason=reason)
|
||||
return was_trade_fully_canceled
|
||||
|
||||
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||
"""
|
||||
Sell cancel - cancel order and update trade
|
||||
:return: Reason for cancel
|
||||
@ -1032,14 +1032,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_sell_cancel(
|
||||
self._notify_exit_cancel(
|
||||
trade,
|
||||
order_type=self.strategy.order_types['sell'],
|
||||
reason=reason
|
||||
)
|
||||
return reason
|
||||
|
||||
def _safe_sell_amount(self, pair: str, amount: float) -> float:
|
||||
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
||||
"""
|
||||
Get sellable amount.
|
||||
Should be trade.amount - but will fall back to the available amount if necessary.
|
||||
@ -1111,7 +1111,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# but we allow this value to be changed)
|
||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||
|
||||
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
@ -1150,11 +1150,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
self._notify_exit(trade, order_type)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
||||
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell occurred.
|
||||
"""
|
||||
@ -1196,7 +1196,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell cancel occurred.
|
||||
"""
|
||||
@ -1291,13 +1291,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Updating wallets when order is closed
|
||||
if not trade.is_open:
|
||||
if not stoploss_order and not trade.open_order_id:
|
||||
self._notify_sell(trade, '', True)
|
||||
self._notify_exit(trade, '', True)
|
||||
self.protections.stop_per_pair(trade.pair)
|
||||
self.protections.global_stop()
|
||||
self.wallets.update()
|
||||
elif not trade.open_order_id:
|
||||
# Buy fill
|
||||
self._notify_buy_fill(trade)
|
||||
self._notify_enter_fill(trade)
|
||||
|
||||
return False
|
||||
|
||||
|
@ -87,7 +87,7 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
||||
# syslog config. The messages should be equal for this.
|
||||
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||
logging.root.addHandler(handler_sl)
|
||||
elif s[0] == 'journald':
|
||||
elif s[0] == 'journald': # pragma: no cover
|
||||
try:
|
||||
from systemd.journal import JournaldLogHandler
|
||||
except ImportError:
|
||||
|
@ -9,7 +9,7 @@ from typing import Any, List
|
||||
|
||||
|
||||
# check min. python version
|
||||
if sys.version_info < (3, 7):
|
||||
if sys.version_info < (3, 7): # pragma: no cover
|
||||
sys.exit("Freqtrade requires Python version >= 3.7")
|
||||
|
||||
from freqtrade.commands import Arguments
|
||||
@ -46,7 +46,7 @@ def main(sysargv: List[str] = None) -> None:
|
||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||
)
|
||||
|
||||
except SystemExit as e:
|
||||
except SystemExit as e: # pragma: no cover
|
||||
return_code = e
|
||||
except KeyboardInterrupt:
|
||||
logger.info('SIGINT received, aborting ...')
|
||||
@ -60,5 +60,5 @@ def main(sysargv: List[str] = None) -> None:
|
||||
sys.exit(return_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
main()
|
||||
|
@ -17,7 +17,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
||||
if keep_invalid:
|
||||
for pair_wc in wildcardpl:
|
||||
try:
|
||||
comp = re.compile(pair_wc)
|
||||
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||
result_partial = [
|
||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||
]
|
||||
@ -33,7 +33,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
||||
else:
|
||||
for pair_wc in wildcardpl:
|
||||
try:
|
||||
comp = re.compile(pair_wc)
|
||||
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||
result += [
|
||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||
]
|
||||
|
@ -5,6 +5,20 @@ import time
|
||||
import uvicorn
|
||||
|
||||
|
||||
def asyncio_setup() -> None: # pragma: no cover
|
||||
# Set eventloop for win32 setups
|
||||
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
|
||||
# via policy.
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 8) and sys.platform == "win32":
|
||||
import asyncio
|
||||
import selectors
|
||||
selector = selectors.SelectSelector()
|
||||
loop = asyncio.SelectorEventLoop(selector)
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
class UvicornServer(uvicorn.Server):
|
||||
"""
|
||||
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
||||
@ -28,7 +42,7 @@ class UvicornServer(uvicorn.Server):
|
||||
try:
|
||||
import uvloop # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
from uvicorn.loops.asyncio import asyncio_setup
|
||||
|
||||
asyncio_setup()
|
||||
else:
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
|
@ -403,6 +403,9 @@ class RPC:
|
||||
# Doing the sum is not right - overall profit needs to be based on initial capital
|
||||
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
||||
starting_balance = self._freqtrade.wallets.get_starting_balance()
|
||||
profit_closed_ratio_fromstart = 0
|
||||
profit_all_ratio_fromstart = 0
|
||||
if starting_balance:
|
||||
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
||||
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
||||
|
||||
@ -545,12 +548,12 @@ class RPC:
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
|
||||
if order['side'] == 'buy':
|
||||
fully_canceled = self._freqtrade.handle_cancel_buy(
|
||||
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||
trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
|
||||
if order['side'] == 'sell':
|
||||
# Cancel order - so it is placed anew with a fresh price.
|
||||
self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
|
||||
if not fully_canceled:
|
||||
# Get current rate and execute sell
|
||||
@ -563,7 +566,7 @@ class RPC:
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
with self._freqtrade._sell_lock:
|
||||
with self._freqtrade._exit_lock:
|
||||
if trade_id == 'all':
|
||||
# Execute sell for all open orders
|
||||
for trade in Trade.get_open_trades():
|
||||
@ -625,7 +628,7 @@ class RPC:
|
||||
Handler for delete <id>.
|
||||
Delete the given trade and close eventually existing open orders.
|
||||
"""
|
||||
with self._freqtrade._sell_lock:
|
||||
with self._freqtrade._exit_lock:
|
||||
c_count = 0
|
||||
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
||||
if not trade:
|
||||
|
@ -1,3 +1,10 @@
|
||||
{%set volume_pairlist = '{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}' %}
|
||||
{
|
||||
"max_open_trades": {{ max_open_trades }},
|
||||
"stake_currency": "{{ stake_currency }}",
|
||||
@ -29,7 +36,7 @@
|
||||
},
|
||||
{{ exchange | indent(4) }},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
|
||||
],
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
|
@ -8,34 +8,8 @@
|
||||
"rateLimit": 200
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ALGO/BTC",
|
||||
"ATOM/BTC",
|
||||
"BAT/BTC",
|
||||
"BCH/BTC",
|
||||
"BRD/BTC",
|
||||
"EOS/BTC",
|
||||
"ETH/BTC",
|
||||
"IOTA/BTC",
|
||||
"LINK/BTC",
|
||||
"LTC/BTC",
|
||||
"NEO/BTC",
|
||||
"NXS/BTC",
|
||||
"XMR/BTC",
|
||||
"XRP/BTC",
|
||||
"XTZ/BTC"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/BTC",
|
||||
"BNB/BUSD",
|
||||
"BNB/ETH",
|
||||
"BNB/EUR",
|
||||
"BNB/NGN",
|
||||
"BNB/PAX",
|
||||
"BNB/RUB",
|
||||
"BNB/TRY",
|
||||
"BNB/TUSD",
|
||||
"BNB/USDC",
|
||||
"BNB/USDS",
|
||||
"BNB/USDT"
|
||||
"BNB/.*"
|
||||
]
|
||||
}
|
||||
|
@ -15,16 +15,6 @@
|
||||
"rateLimit": 500
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ETH/BTC",
|
||||
"LTC/BTC",
|
||||
"ETC/BTC",
|
||||
"DASH/BTC",
|
||||
"ZEC/BTC",
|
||||
"XLM/BTC",
|
||||
"XRP/BTC",
|
||||
"TRX/BTC",
|
||||
"ADA/BTC",
|
||||
"XMR/BTC"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
]
|
||||
|
@ -7,28 +7,10 @@
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 1000
|
||||
// Enable the below for downoading data.
|
||||
//"rateLimit": 3100
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ADA/EUR",
|
||||
"ATOM/EUR",
|
||||
"BAT/EUR",
|
||||
"BCH/EUR",
|
||||
"BTC/EUR",
|
||||
"DAI/EUR",
|
||||
"DASH/EUR",
|
||||
"EOS/EUR",
|
||||
"ETC/EUR",
|
||||
"ETH/EUR",
|
||||
"LINK/EUR",
|
||||
"LTC/EUR",
|
||||
"QTUM/EUR",
|
||||
"REP/EUR",
|
||||
"WAVES/EUR",
|
||||
"XLM/EUR",
|
||||
"XMR/EUR",
|
||||
"XRP/EUR",
|
||||
"XTZ/EUR",
|
||||
"ZEC/EUR"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
|
||||
|
18
freqtrade/templates/subtemplates/exchange_kucoin.j2
Normal file
18
freqtrade/templates/subtemplates/exchange_kucoin.j2
Normal file
@ -0,0 +1,18 @@
|
||||
"exchange": {
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"password": "{{ exchange_key_password }}",
|
||||
"ccxt_config": {
|
||||
"enableRateLimit": true
|
||||
"rateLimit": 200
|
||||
},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 200
|
||||
},
|
||||
"pair_whitelist": [
|
||||
],
|
||||
"pair_blacklist": [
|
||||
]
|
||||
}
|
@ -20,14 +20,14 @@ nav:
|
||||
- Web Hook: webhook-config.md
|
||||
- Data Downloading: data-download.md
|
||||
- Backtesting: backtesting.md
|
||||
- Leverage: leverage.md
|
||||
- Hyperopt: hyperopt.md
|
||||
- Leverage: leverage.md
|
||||
- Utility Sub-commands: utils.md
|
||||
- Plotting: plotting.md
|
||||
- Exchange-specific Notes: exchanges.md
|
||||
- Data Analysis:
|
||||
- Jupyter Notebooks: data-analysis.md
|
||||
- Strategy analysis: strategy_analysis_example.md
|
||||
- Exchange-specific Notes: exchanges.md
|
||||
- Advanced Topics:
|
||||
- Advanced Post-installation Tasks: advanced-setup.md
|
||||
- Edge Positioning: edge.md
|
||||
|
@ -8,7 +8,7 @@ flake8==3.9.2
|
||||
flake8-type-annotations==0.1.0
|
||||
flake8-tidy-imports==4.4.1
|
||||
mypy==0.910
|
||||
pytest==6.2.4
|
||||
pytest==6.2.5
|
||||
pytest-asyncio==0.15.1
|
||||
pytest-cov==2.12.1
|
||||
pytest-mock==3.6.1
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.3.0
|
||||
plotly==5.3.1
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
numpy==1.21.2
|
||||
pandas==1.3.2
|
||||
|
||||
ccxt==1.55.56
|
||||
ccxt==1.55.83
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
cryptography==3.4.8
|
||||
aiohttp==3.7.4.post0
|
||||
|
14
setup.sh
14
setup.sh
@ -95,19 +95,7 @@ function install_talib() {
|
||||
return
|
||||
fi
|
||||
|
||||
cd build_helpers
|
||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
cd ta-lib
|
||||
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||
./configure --prefix=/usr/local
|
||||
make
|
||||
sudo make install
|
||||
if [ -x "$(command -v apt-get)" ]; then
|
||||
echo "Updating library path using ldconfig"
|
||||
sudo ldconfig
|
||||
fi
|
||||
cd .. && rm -rf ./ta-lib/
|
||||
cd ..
|
||||
cd build_helpers && ./install_ta-lib.sh && cd ..
|
||||
}
|
||||
|
||||
function install_mac_newer_python_dependencies() {
|
||||
|
@ -108,6 +108,13 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
assert hasattr(ex._api_async, 'TestKWARG')
|
||||
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
||||
assert log_has(asynclogmsg, caplog)
|
||||
# Test additional headers case
|
||||
Exchange._headers = {'hello': 'world'}
|
||||
ex = Exchange(conf)
|
||||
|
||||
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
||||
assert ex._api.headers == {'hello': 'world'}
|
||||
Exchange._headers = {}
|
||||
|
||||
|
||||
def test_destroy(default_conf, mocker, caplog):
|
||||
|
@ -739,11 +739,16 @@ def test_auto_hyperopt_interface(default_conf):
|
||||
PairLocks.timeframe = default_conf['timeframe']
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
next(strategy.enumerate_parameters('deadBeef'))
|
||||
|
||||
assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi']
|
||||
# PlusDI is NOT in the buy-params, so default should be used
|
||||
assert strategy.buy_plusdi.value == 0.5
|
||||
assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi']
|
||||
|
||||
assert repr(strategy.sell_rsi) == 'IntParameter(74)'
|
||||
|
||||
# Parameter is disabled - so value from sell_param dict will NOT be used.
|
||||
assert strategy.sell_minusdi.value == 0.5
|
||||
all_params = strategy.detect_all_parameters()
|
||||
|
@ -518,6 +518,7 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order,
|
||||
# 0 trades, but it's not because of pairlock.
|
||||
assert n == 0
|
||||
assert not log_has_re(message, caplog)
|
||||
caplog.clear()
|
||||
|
||||
PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because')
|
||||
n = freqtrade.enter_positions()
|
||||
@ -1086,6 +1087,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
||||
assert trade.stoploss_order_id is None
|
||||
assert trade.is_open is False
|
||||
caplog.clear()
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Binance.stoploss',
|
||||
@ -1190,7 +1192,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
||||
assert trade.stoploss_order_id is None
|
||||
assert trade.sell_reason == SellType.EMERGENCY_SELL.value
|
||||
assert log_has("Unable to place a stoploss order on exchange. ", caplog)
|
||||
assert log_has("Selling the trade forcefully", caplog)
|
||||
assert log_has("Exiting the trade forcefully", caplog)
|
||||
|
||||
# Should call a market sell
|
||||
assert create_order_mock.call_count == 2
|
||||
@ -1743,10 +1745,12 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
|
||||
)
|
||||
assert not freqtrade.update_trade_state(trade, None)
|
||||
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
||||
caplog.clear()
|
||||
# Add datetime explicitly since sqlalchemy defaults apply only once written to database
|
||||
freqtrade.update_trade_state(trade, '123')
|
||||
# Test amount not modified by fee-logic
|
||||
assert not log_has_re(r'Applying fee to .*', caplog)
|
||||
caplog.clear()
|
||||
assert trade.open_order_id is None
|
||||
assert trade.amount == limit_buy_order['amount']
|
||||
|
||||
@ -2453,8 +2457,8 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
handle_cancel_buy=MagicMock(),
|
||||
handle_cancel_sell=MagicMock(),
|
||||
handle_cancel_enter=MagicMock(),
|
||||
handle_cancel_exit=MagicMock(),
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -2475,7 +2479,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
|
||||
caplog)
|
||||
|
||||
|
||||
def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None:
|
||||
def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
cancel_buy_order = deepcopy(limit_buy_order)
|
||||
@ -2486,7 +2490,7 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
|
||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade._notify_buy_cancel = MagicMock()
|
||||
freqtrade._notify_enter_cancel = MagicMock()
|
||||
|
||||
trade = MagicMock()
|
||||
trade.pair = 'LTC/USDT'
|
||||
@ -2494,46 +2498,46 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
|
||||
limit_buy_order['filled'] = 0.0
|
||||
limit_buy_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||
assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
cancel_order_mock.reset_mock()
|
||||
caplog.clear()
|
||||
limit_buy_order['filled'] = 0.01
|
||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog)
|
||||
|
||||
caplog.clear()
|
||||
cancel_order_mock.reset_mock()
|
||||
limit_buy_order['filled'] = 2
|
||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
# Order remained open for some reason (cancel failed)
|
||||
cancel_buy_order['status'] = 'open'
|
||||
cancel_order_mock = MagicMock(return_value=cancel_buy_order)
|
||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
||||
indirect=['limit_buy_order_canceled_empty'])
|
||||
def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf,
|
||||
def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf,
|
||||
limit_buy_order_canceled_empty) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
cancel_order_mock = mocker.patch(
|
||||
'freqtrade.exchange.Exchange.cancel_order_with_result',
|
||||
return_value=limit_buy_order_canceled_empty)
|
||||
nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel')
|
||||
nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel')
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
trade = MagicMock()
|
||||
trade.pair = 'LTC/ETH'
|
||||
assert freqtrade.handle_cancel_buy(trade, limit_buy_order_canceled_empty, reason)
|
||||
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog)
|
||||
assert nofiy_mock.call_count == 1
|
||||
@ -2545,7 +2549,7 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf,
|
||||
'String Return value',
|
||||
123
|
||||
])
|
||||
def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
||||
def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
|
||||
cancelorder) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
@ -2556,7 +2560,7 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade._notify_buy_cancel = MagicMock()
|
||||
freqtrade._notify_enter_cancel = MagicMock()
|
||||
|
||||
trade = MagicMock()
|
||||
trade.pair = 'LTC/USDT'
|
||||
@ -2564,16 +2568,16 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
|
||||
limit_buy_order['filled'] = 0.0
|
||||
limit_buy_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||
assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
cancel_order_mock.reset_mock()
|
||||
limit_buy_order['filled'] = 1.0
|
||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
|
||||
def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
|
||||
def test_handle_cancel_exit_limit(mocker, default_conf, fee) -> None:
|
||||
send_msg_mock = patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
@ -2599,26 +2603,26 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
|
||||
'amount': 1,
|
||||
'status': "open"}
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_sell(trade, order, reason)
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
|
||||
send_msg_mock.reset_mock()
|
||||
|
||||
order['amount'] = 2
|
||||
assert freqtrade.handle_cancel_sell(trade, order, reason
|
||||
assert 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_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert freqtrade.handle_cancel_sell(trade, order, reason
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason
|
||||
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
# Message should not be iterated again
|
||||
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
assert send_msg_mock.call_count == 1
|
||||
|
||||
|
||||
def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
|
||||
def test_handle_cancel_exit_cancel_exception(mocker, default_conf) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
@ -2631,7 +2635,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
|
||||
order = {'remaining': 1,
|
||||
'amount': 1,
|
||||
'status': "open"}
|
||||
assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order'
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order'
|
||||
|
||||
|
||||
def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
|
||||
@ -3303,7 +3307,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_
|
||||
assert trade.amount != amnt
|
||||
|
||||
|
||||
def test__safe_sell_amount(default_conf, fee, caplog, mocker):
|
||||
def test__safe_exit_amount(default_conf, fee, caplog, mocker):
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
amount = 95.33
|
||||
@ -3323,17 +3327,17 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker):
|
||||
patch_get_signal(freqtrade)
|
||||
|
||||
wallet_update.reset_mock()
|
||||
assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet
|
||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
|
||||
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||
assert wallet_update.call_count == 1
|
||||
caplog.clear()
|
||||
wallet_update.reset_mock()
|
||||
assert freqtrade._safe_sell_amount(trade.pair, amount_wallet) == amount_wallet
|
||||
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
|
||||
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||
assert wallet_update.call_count == 1
|
||||
|
||||
|
||||
def test__safe_sell_amount_error(default_conf, fee, caplog, mocker):
|
||||
def test__safe_exit_amount_error(default_conf, fee, caplog, mocker):
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
amount = 95.33
|
||||
@ -3351,7 +3355,7 @@ def test__safe_sell_amount_error(default_conf, fee, caplog, mocker):
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
with pytest.raises(DependencyException, match=r"Not enough amount to sell."):
|
||||
assert freqtrade._safe_sell_amount(trade.pair, trade.amount)
|
||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
|
||||
|
||||
|
||||
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
|
||||
@ -3525,6 +3529,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_or
|
||||
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog)
|
||||
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert trade.stop_loss == 0.0000138501
|
||||
caplog.clear()
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
MagicMock(return_value={
|
||||
@ -3585,6 +3590,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde
|
||||
assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog)
|
||||
assert log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert trade.stop_loss == 0.0000138501
|
||||
caplog.clear()
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
MagicMock(return_value={
|
||||
@ -3649,6 +3655,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_
|
||||
|
||||
assert not log_has("ETH/BTC - Adjusting stoploss...", caplog)
|
||||
assert trade.stop_loss == 0.0000098910
|
||||
caplog.clear()
|
||||
|
||||
# price rises above the offset (rises 12% when the offset is 5.5%)
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
@ -4316,8 +4323,8 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
|
||||
side_effect=[
|
||||
ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order])
|
||||
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy')
|
||||
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell')
|
||||
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter')
|
||||
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit')
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
create_mock_trades(fee)
|
||||
@ -4351,6 +4358,7 @@ def test_update_open_orders(mocker, default_conf, fee, caplog):
|
||||
|
||||
freqtrade.update_open_orders()
|
||||
assert not log_has_re(r"Error updating Order .*", caplog)
|
||||
caplog.clear()
|
||||
|
||||
freqtrade.config['dry_run'] = False
|
||||
freqtrade.update_open_orders()
|
||||
@ -4432,14 +4440,14 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee):
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
|
||||
def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
|
||||
|
||||
create_mock_trades(fee)
|
||||
trades = Trade.get_trades().all()
|
||||
|
||||
freqtrade.reupdate_buy_order_fees(trades[0])
|
||||
freqtrade.reupdate_enter_order_fees(trades[0])
|
||||
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
|
||||
assert mock_uts.call_count == 1
|
||||
assert mock_uts.call_args_list[0][0][0] == trades[0]
|
||||
@ -4462,7 +4470,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
|
||||
)
|
||||
Trade.query.session.add(trade)
|
||||
|
||||
freqtrade.reupdate_buy_order_fees(trade)
|
||||
freqtrade.reupdate_enter_order_fees(trade)
|
||||
assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
|
||||
assert mock_uts.call_count == 0
|
||||
assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog)
|
||||
@ -4472,7 +4480,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
|
||||
def test_handle_insufficient_funds(mocker, default_conf, fee):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order')
|
||||
mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees')
|
||||
mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees')
|
||||
create_mock_trades(fee)
|
||||
trades = Trade.get_trades().all()
|
||||
|
||||
|
@ -70,7 +70,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
create_stoploss_order=MagicMock(return_value=True),
|
||||
_notify_sell=MagicMock(),
|
||||
_notify_exit=MagicMock(),
|
||||
)
|
||||
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
|
||||
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
|
||||
@ -154,7 +154,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
create_stoploss_order=MagicMock(return_value=True),
|
||||
_notify_sell=MagicMock(),
|
||||
_notify_exit=MagicMock(),
|
||||
)
|
||||
should_sell_mock = MagicMock(side_effect=[
|
||||
SellCheckTuple(sell_type=SellType.NONE),
|
||||
|
Loading…
Reference in New Issue
Block a user