merge upstream

This commit is contained in:
மனோஜ்குமார் பழனிச்சாமி
2022-05-08 13:14:07 +05:30
29 changed files with 772 additions and 129 deletions

View File

@@ -483,6 +483,8 @@ CANCEL_REASON = {
"ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)",
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
"FORCE_EXIT": "forcesold",
"REPLACE": "cancelled to be replaced by new limit order",
"USER_CANCEL": "user requested order cancel"
}
# List of pairs with their timeframes
@@ -494,3 +496,4 @@ TradeList = List[List]
LongShort = Literal['long', 'short']
EntryExit = Literal['entry', 'exit']
BuySell = Literal['buy', 'sell']

View File

@@ -20,7 +20,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
decimal_to_precision)
from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
EntryExit, ListPairsWithTimeframes, PairWithTimeframe)
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
@@ -198,6 +198,7 @@ class Exchange:
if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_tiers()
self.additional_exchange_init()
def __del__(self):
"""
@@ -294,6 +295,14 @@ class Exchange:
"""exchange ccxt precisionMode"""
return self._api.precisionMode
def additional_exchange_init(self) -> None:
"""
Additional exchange initialization logic.
.api will be available at this point.
Must be overridden in child methods if required.
"""
pass
def _log_exchange_response(self, endpoint, response) -> None:
""" Log exchange responses """
if self.log_responses:
@@ -937,13 +946,14 @@ class Exchange:
# Order handling
def _lev_prep(self, pair: str, leverage: float, side: str):
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
if self.trading_mode != TradingMode.SPOT:
self.set_margin_mode(pair, self.margin_mode)
self._set_leverage(leverage, pair)
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
@@ -962,7 +972,7 @@ class Exchange:
*,
pair: str,
ordertype: str,
side: str,
side: BuySell,
amount: float,
rate: float,
leverage: float,
@@ -973,7 +983,7 @@ class Exchange:
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage)
return dry_order
params = self._get_params(ordertype, leverage, reduceOnly, time_in_force)
params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
try:
# Set the precision for amount and price(rate) as accepted by the exchange
@@ -1058,7 +1068,7 @@ class Exchange:
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
side: str, leverage: float) -> Dict:
side: BuySell, leverage: float) -> Dict:
"""
creates a stoploss order.
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market

View File

@@ -4,6 +4,7 @@ from typing import Any, Dict, List, Tuple
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError)
@@ -44,7 +45,7 @@ class Ftx(Exchange):
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
order_types: Dict, side: BuySell, leverage: float) -> Dict:
"""
Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit order.

View File

@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple
import ccxt
from pandas import DataFrame
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError)
@@ -95,7 +96,7 @@ class Kraken(Exchange):
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
order_types: Dict, side: BuySell, leverage: float) -> Dict:
"""
Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken.
@@ -165,12 +166,14 @@ class Kraken(Exchange):
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc'
) -> Dict:
params = super()._get_params(
side=side,
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,

View File

@@ -3,6 +3,7 @@ from typing import Dict, List, Tuple
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
@@ -34,14 +35,48 @@ class Okx(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
net_only = True
@retrier
def additional_exchange_init(self) -> None:
"""
Additional exchange initialization logic.
.api will be available at this point.
Must be overridden in child methods if required.
"""
try:
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
accounts = self._api.fetch_accounts()
if len(accounts) > 0:
self.net_only = accounts[0].get('info', {}).get('posMode') == 'net_mode'
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def _get_posSide(self, side: BuySell, reduceOnly: bool):
if self.net_only:
return 'net'
if not reduceOnly:
# Enter
return 'long' if side == 'buy' else 'short'
else:
# Exit
return 'long' if side == 'sell' else 'short'
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc',
) -> Dict:
params = super()._get_params(
side=side,
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
@@ -49,10 +84,11 @@ class Okx(Exchange):
)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, reduceOnly)
return params
@retrier
def _lev_prep(self, pair: str, leverage: float, side: str):
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try:
# TODO-lev: Test me properly (check mgnMode passed)
@@ -61,7 +97,7 @@ class Okx(Exchange):
symbol=pair,
params={
"mgnMode": self.margin_mode.value,
# "posSide": "net"",
"posSide": self._get_posSide(side, False),
})
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e

View File

@@ -14,7 +14,7 @@ from schedule import Scheduler
from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency
from freqtrade.constants import LongShort
from freqtrade.constants import BuySell, LongShort
from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge
@@ -23,6 +23,7 @@ from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode,
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
@@ -191,8 +192,8 @@ class FreqtradeBot(LoggingMixin):
self.strategy.analyze(self.active_pair_whitelist)
with self._exit_lock:
# Check and handle any timed out open orders
self.check_handle_timedout()
# Check for exchange cancelations, timeouts and user requested replace
self.manage_open_orders()
# Protect from collisions with force_exit.
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
@@ -616,7 +617,8 @@ class FreqtradeBot(LoggingMixin):
"""
time_in_force = self.strategy.order_time_in_force['entry']
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
side: BuySell = 'sell' if is_short else 'buy'
name = 'Short' if is_short else 'Long'
trade_side: LongShort = 'short' if is_short else 'long'
pos_adjust = trade is not None
@@ -1140,13 +1142,13 @@ class FreqtradeBot(LoggingMixin):
return True
return False
def check_handle_timedout(self) -> None:
def manage_open_orders(self) -> None:
"""
Check if any orders are timed out and cancel if necessary
:param timeoutvalue: Number of minutes until order is considered timed out
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
was met or replaced if there's a new candle and user has requested it.
Timeout setting takes priority over limit order adjustment request.
:return: None
"""
for trade in Trade.get_open_order_trades():
try:
if not trade.open_order_id:
@@ -1157,33 +1159,87 @@ class FreqtradeBot(LoggingMixin):
continue
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
is_entering = order['side'] == trade.entry_side
not_closed = order['status'] == 'open' or fully_cancelled
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
order_obj = trade.select_order_by_order_id(trade.open_order_id)
if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc)))
):
if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
if not_closed:
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc))):
self.handle_timedout_order(order, trade)
else:
canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get(
'unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergency exiting trade {trade}, as the exit order '
f'timed out {max_timeouts} times.')
try:
self.execute_trade_exit(
trade, order.get('price'),
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
except DependencyException as exception:
logger.warning(
f'Unable to emergency sell trade {trade.pair}: {exception}')
self.replace_order(order, order_obj, trade)
def handle_timedout_order(self, order: Dict, trade: Trade) -> None:
"""
Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order()
:param trade: Trade object.
:return: None
"""
if order['side'] == trade.entry_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
else:
canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergency exiting trade {trade}, as the exit order '
f'timed out {max_timeouts} times.')
try:
self.execute_trade_exit(
trade, order['price'],
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
except DependencyException as exception:
logger.warning(
f'Unable to emergency sell trade {trade.pair}: {exception}')
def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None:
"""
Check if current analyzed entry order should be replaced or simply cancelled.
To simply cancel the existing order(no replacement) adjust_entry_price() should return None
To maintain existing order adjust_entry_price() should return order_obj.price
To replace existing order adjust_entry_price() should return desired price for limit order
:param order: Order dict grabbed with exchange.fetch_order()
:param order_obj: Order object.
:param trade: Trade object.
:return: None
"""
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe)
latest_candle_open_date = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None
latest_candle_close_date = timeframe_to_next_date(self.strategy.timeframe,
latest_candle_open_date)
# Check if new candle
if order_obj and latest_candle_close_date > order_obj.order_date_utc:
# New candle
proposed_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
adjusted_entry_price = strategy_safe_wrapper(self.strategy.adjust_entry_price,
default_retval=order_obj.price)(
trade=trade, order=order_obj, pair=trade.pair,
current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
current_order_rate=order_obj.price, entry_tag=trade.enter_tag,
side=trade.entry_side)
full_cancel = False
cancel_reason = constants.CANCEL_REASON['REPLACE']
if not adjusted_entry_price:
full_cancel = True if trade.nr_of_successful_entries == 0 else False
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
if order_obj.price != adjusted_entry_price:
# cancel existing order if new price is supplied or None
self.handle_cancel_enter(trade, order, cancel_reason,
allow_full_cancel=full_cancel)
if adjusted_entry_price:
# place new order only if new price is supplied
self.execute_entry(
pair=trade.pair,
stake_amount=(order_obj.remaining * order_obj.price),
price=adjusted_entry_price,
trade=trade,
is_short=trade.is_short
)
def cancel_all_open_orders(self) -> None:
"""
@@ -1205,7 +1261,10 @@ class FreqtradeBot(LoggingMixin):
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
Trade.commit()
def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool:
def handle_cancel_enter(
self, trade: Trade, order: Dict, reason: str,
allow_full_cancel: Optional[bool] = True
) -> bool:
"""
Buy cancel - cancel order
:return: True if order was fully cancelled
@@ -1243,9 +1302,10 @@ class FreqtradeBot(LoggingMixin):
# Using filled to determine the filled amount
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
# if trade is not partially completed and it's the only order, just delete the trade
if len(trade.orders) <= 1:
open_order_count = len([order for order in trade.orders if order.status == 'open'])
if open_order_count <= 1 and allow_full_cancel:
logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
trade.delete()
was_trade_fully_canceled = True
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
@@ -1622,7 +1682,6 @@ class FreqtradeBot(LoggingMixin):
# TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate()
trade.set_isolated_liq(self.exchange.get_liquidation_price(
leverage=trade.leverage,
pair=trade.pair,
amount=trade.amount,

View File

@@ -732,19 +732,25 @@ class Backtesting:
def _enter_trade(self, pair: str, row: Tuple, direction: LongShort,
stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
trade: Optional[LocalTrade] = None,
requested_rate: Optional[float] = None,
requested_stake: Optional[float] = None) -> Optional[LocalTrade]:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price
order_type = self.strategy.order_types['entry']
pos_adjust = trade is not None
pos_adjust = trade is not None and requested_rate is None
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
order_type
)
# replace proposed rate if another rate was requested
propose_rate = requested_rate if requested_rate else propose_rate
stake_amount = requested_stake if requested_stake else stake_amount
if not stake_amount:
# In case of pos adjust, still return the original trade
# If not pos adjust, trade is None
@@ -826,7 +832,7 @@ class Backtesting:
cost=stake_amount + trade.fee_open,
)
if pos_adjust and self._get_order_filled(order.price, row):
order.close_bt_order(current_time)
order.close_bt_order(current_time, trade)
else:
trade.open_order_id = str(self.order_id_counter)
trade.orders.append(order)
@@ -886,30 +892,78 @@ class Backtesting:
self.protections.stop_per_pair(pair, current_time, side)
self.protections.global_stop(current_time, side)
def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
def manage_open_orders(self, trade: LocalTrade, current_time, row: Tuple) -> bool:
"""
Check if an order has been canceled.
Returns True if the trade should be Deleted (initial order was canceled).
Check if any open order needs to be cancelled or replaced.
Returns True if the trade should be deleted.
"""
for order in [o for o in trade.orders if o.ft_is_open]:
if self.check_order_cancel(trade, order, current_time):
# delete trade due to order timeout
return True
elif self.check_order_replace(trade, order, current_time, row):
# delete trade due to user request
return True
# default maintain trade
return False
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
if timedout:
if order.side == trade.entry_side:
self.timedout_entry_orders += 1
if trade.nr_of_successful_entries == 0:
# Remove trade due to entry timeout expiration.
return True
else:
# Close additional entry order
del trade.orders[trade.orders.index(order)]
if order.side == trade.exit_side:
self.timedout_exit_orders += 1
# Close exit order and retry exiting on next signal.
def check_order_cancel(self, trade: LocalTrade, order: Order, current_time) -> bool:
"""
Check if current analyzed order has to be canceled.
Returns True if the trade should be Deleted (initial order was canceled).
"""
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
if timedout:
if order.side == trade.entry_side:
self.timedout_entry_orders += 1
if trade.nr_of_successful_entries == 0:
# Remove trade due to entry timeout expiration.
return True
else:
# Close additional entry order
del trade.orders[trade.orders.index(order)]
if order.side == trade.exit_side:
self.timedout_exit_orders += 1
# Close exit order and retry exiting on next signal.
del trade.orders[trade.orders.index(order)]
return False
def check_order_replace(self, trade: LocalTrade, order: Order, current_time,
row: Tuple) -> bool:
"""
Check if current analyzed entry order has to be replaced and do so.
If user requested cancellation and there are no filled orders in the trade will
instruct caller to delete the trade.
Returns True if the trade should be deleted.
"""
# only check on new candles for open entry orders
if order.side == trade.entry_side and current_time > order.order_date_utc:
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
default_retval=order.price)(
trade=trade, order=order, pair=trade.pair, current_time=current_time,
proposed_rate=row[OPEN_IDX], current_order_rate=order.price,
entry_tag=trade.enter_tag, side=trade.trade_direction
) # default value is current order price
# cancel existing order whenever a new rate is requested (or None)
if requested_rate == order.price:
# assumption: there can't be multiple open entry orders at any given time
return False
else:
del trade.orders[trade.orders.index(order)]
# place new order if result was not None
if requested_rate:
self._enter_trade(pair=trade.pair, row=row, trade=trade,
requested_rate=requested_rate,
requested_stake=(order.remaining * order.price),
direction='short' if trade.is_short else 'long')
else:
# assumption: there can't be multiple open entry orders at any given time
return (trade.nr_of_successful_entries == 0)
return False
def validate_row(
self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]:
try:
@@ -979,9 +1033,9 @@ class Backtesting:
self.dataprovider._set_dataframe_max_index(row_index)
for t in list(open_trades[pair]):
# 1. Cancel expired entry/exit orders.
if self.check_order_cancel(t, current_time):
# Close trade due to entry timeout expiration.
# 1. Manage currently open orders of active trades
if self.manage_open_orders(t, current_time, row):
# Close trade
open_trade_count -= 1
open_trades[pair].remove(t)
self.wallets.update()
@@ -1012,7 +1066,7 @@ class Backtesting:
# 3. Process entry orders.
order = trade.select_order(trade.entry_side, is_open=True)
if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time)
order.close_bt_order(current_time, trade)
trade.open_order_id = None
LocalTrade.add_bt_trade(trade)
self.wallets.update()
@@ -1027,7 +1081,7 @@ class Backtesting:
trade.open_order_id = None
sub_trade = order.safe_amount_after_fee != trade.amount
if sub_trade:
order.close_bt_order(current_time)
order.close_bt_order(current_time, trade)
trade.process_exit_sub_trade(order)
trade.recalc_trade_from_orders()
else:

View File

@@ -15,7 +15,7 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
LongShort)
BuySell, LongShort)
from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest
@@ -221,11 +221,15 @@ class Order(_DECL_BASE):
'remaining': self.remaining,
}
def close_bt_order(self, close_date: datetime):
def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
self.order_filled_date = close_date
self.filled = self.amount
self.status = 'closed'
self.ft_is_open = False
if (self.ft_order_side == trade.entry_side
and len(trade.select_filled_orders(trade.entry_side)) == 1):
trade.open_rate = self.price
trade.recalc_open_trade_value()
@staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]):
@@ -389,7 +393,7 @@ class LocalTrade():
return "buy"
@property
def exit_side(self) -> str:
def exit_side(self) -> BuySell:
if self.is_short:
return "buy"
else:

View File

@@ -634,6 +634,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange)
strategy.bot_start()
strategy.bot_loop_start()
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange']
trades = plot_elements['trades']

View File

@@ -21,6 +21,7 @@ class LowProfitPairs(IProtection):
self._trade_limit = protection_config.get('trade_limit', 1)
self._required_profit = protection_config.get('required_profit', 0.0)
self._only_per_side = protection_config.get('only_per_side', False)
def short_desc(self) -> str:
"""
@@ -36,7 +37,8 @@ class LowProfitPairs(IProtection):
return (f'{profit} < {self._required_profit} in {self.lookback_period_str}, '
f'locking for {self.stop_duration_str}.')
def _low_profit(self, date_now: datetime, pair: str) -> Optional[ProtectionReturn]:
def _low_profit(
self, date_now: datetime, pair: str, side: LongShort) -> Optional[ProtectionReturn]:
"""
Evaluate recent trades for pair
"""
@@ -54,7 +56,10 @@ class LowProfitPairs(IProtection):
# Not enough trades in the relevant period
return None
profit = sum(trade.close_profit for trade in trades if trade.close_profit)
profit = sum(
trade.close_profit for trade in trades if trade.close_profit
and (not self._only_per_side or trade.trade_direction == side)
)
if profit < self._required_profit:
self.log_once(
f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} "
@@ -65,6 +70,7 @@ class LowProfitPairs(IProtection):
lock=True,
until=until,
reason=self._reason(profit),
lock_side=(side if self._only_per_side else '*')
)
return None
@@ -86,4 +92,4 @@ class LowProfitPairs(IProtection):
:return: Tuple of [bool, until, reason].
If true, this pair will be locked with <reason> until <until>
"""
return self._low_profit(date_now, pair=pair)
return self._low_profit(date_now, pair=pair, side=side)

View File

@@ -38,8 +38,8 @@ class StoplossGuard(IProtection):
return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, '
f'locking for {self._stop_duration} min.')
def _stoploss_guard(
self, date_now: datetime, pair: Optional[str], side: str) -> Optional[ProtectionReturn]:
def _stoploss_guard(self, date_now: datetime, pair: Optional[str],
side: LongShort) -> Optional[ProtectionReturn]:
"""
Evaluate recent trades
"""

View File

@@ -172,6 +172,7 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"status_msg": "Backtest running",
}
if ApiServer._bt:
ApiServer._bt.cleanup()
del ApiServer._bt
ApiServer._bt = None
del ApiServer._bt_data

View File

@@ -471,6 +471,34 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return None
def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str,
current_time: datetime, proposed_rate: float, current_order_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
"""
Entry price re-adjustment logic, returning the user desired limit price.
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
If None is returned then order gets canceled but not replaced by a new one.
:param pair: Pair that's currently analyzed
:param trade: Trade object.
:param order: Order object
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
:param current_order_rate: Rate of the existing order in place.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided
"""
return current_order_rate
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:

View File

@@ -4,7 +4,9 @@
# --- Do not remove these libs ---
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from pandas import DataFrame # noqa
from datetime import datetime # noqa
from typing import Optional # noqa
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter)

View File

@@ -13,7 +13,7 @@ def bot_loop_start(self, **kwargs) -> None:
pass
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float,
entry_tag: 'Optional[str]', **kwargs) -> float:
entry_tag: Optional[str], **kwargs) -> float:
"""
Custom entry price logic, returning the new entry price.
@@ -30,6 +30,34 @@ def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate:
"""
return proposed_rate
def adjust_entry_price(self, trade: 'Trade', order: 'Optional[Order]', pair: str,
current_time: datetime, proposed_rate: float, current_order_rate: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
"""
Entry price re-adjustment logic, returning the user desired limit price.
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
If None is returned then order gets canceled but not replaced by a new one.
:param pair: Pair that's currently analyzed
:param trade: Trade object.
:param order: Order object
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
:param current_order_rate: Rate of the existing order in place.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided
"""
return current_order_rate
def custom_exit_price(self, pair: str, trade: 'Trade',
current_time: 'datetime', proposed_rate: float,
current_profit: float, exit_tag: Optional[str], **kwargs) -> float:
@@ -53,7 +81,7 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
side: str, entry_tag: 'Optional[str]', **kwargs) -> float:
side: str, entry_tag: Optional[str], **kwargs) -> float:
"""
Customize stake size for each new trade.
@@ -118,7 +146,7 @@ def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: 'Optional[str]',
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
side: str, **kwargs) -> bool:
"""
Called right before placing a entry order.
@@ -217,7 +245,7 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
current_rate: float, current_profit: float, min_stake: float,
max_stake: float, **kwargs) -> 'Optional[float]':
max_stake: float, **kwargs) -> Optional[float]:
"""
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
This means extra buy orders with additional fees.