commit
d6773bc32c
@ -667,7 +667,7 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
|
|
||||||
# This is called when placing the initial order (opening trade)
|
# This is called when placing the initial order (opening trade)
|
||||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
|
||||||
# We need to leave most of the funds for possible further DCA orders
|
# We need to leave most of the funds for possible further DCA orders
|
||||||
@ -675,7 +675,7 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
return proposed_stake / self.max_dca_multiplier
|
return proposed_stake / self.max_dca_multiplier
|
||||||
|
|
||||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||||
current_rate: float, current_profit: float, min_stake: float,
|
current_rate: float, current_profit: float, min_stake: Optional[float],
|
||||||
max_stake: float, **kwargs):
|
max_stake: float, **kwargs):
|
||||||
"""
|
"""
|
||||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||||
|
@ -199,7 +199,7 @@ New string argument `side` - which can be either `"long"` or `"short"`.
|
|||||||
``` python hl_lines="4"
|
``` python hl_lines="4"
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||||
entry_tag: Optional[str], **kwargs) -> float:
|
entry_tag: Optional[str], **kwargs) -> float:
|
||||||
# ...
|
# ...
|
||||||
return proposed_stake
|
return proposed_stake
|
||||||
@ -208,7 +208,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
``` python hl_lines="4"
|
``` python hl_lines="4"
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
# ...
|
# ...
|
||||||
return proposed_stake
|
return proposed_stake
|
||||||
|
@ -27,7 +27,7 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
|||||||
return True
|
return True
|
||||||
logger.info("Checking exchange...")
|
logger.info("Checking exchange...")
|
||||||
|
|
||||||
exchange = config.get('exchange', {}).get('name').lower()
|
exchange = config.get('exchange', {}).get('name', '').lower()
|
||||||
if not exchange:
|
if not exchange:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'This command requires a configured exchange. You should either use '
|
f'This command requires a configured exchange. You should either use '
|
||||||
|
@ -282,6 +282,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
pairs_not_available = []
|
pairs_not_available = []
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
candle_type = CandleType.get_default(trading_mode)
|
candle_type = CandleType.get_default(trading_mode)
|
||||||
|
process = ''
|
||||||
for idx, pair in enumerate(pairs, start=1):
|
for idx, pair in enumerate(pairs, start=1):
|
||||||
if pair not in exchange.markets:
|
if pair not in exchange.markets:
|
||||||
pairs_not_available.append(pair)
|
pairs_not_available.append(pair)
|
||||||
|
@ -2,6 +2,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from typing import Any, Callable, Optional, TypeVar, cast, overload
|
||||||
|
|
||||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
@ -133,8 +134,22 @@ def retrier_async(f):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def retrier(_func=None, retries=API_RETRY_COUNT):
|
F = TypeVar('F', bound=Callable[..., Any])
|
||||||
def decorator(f):
|
|
||||||
|
|
||||||
|
# Type shenanigans
|
||||||
|
@overload
|
||||||
|
def retrier(_func: F) -> F:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT):
|
||||||
|
def decorator(f: F) -> F:
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
count = kwargs.pop('count', retries)
|
count = kwargs.pop('count', retries)
|
||||||
@ -155,7 +170,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
|
|||||||
else:
|
else:
|
||||||
logger.warning(msg + 'Giving up.')
|
logger.warning(msg + 'Giving up.')
|
||||||
raise ex
|
raise ex
|
||||||
return wrapper
|
return cast(F, wrapper)
|
||||||
# Support both @retrier and @retrier(retries=2) syntax
|
# Support both @retrier and @retrier(retries=2) syntax
|
||||||
if _func is None:
|
if _func is None:
|
||||||
return decorator
|
return decorator
|
||||||
|
@ -92,8 +92,8 @@ class Exchange:
|
|||||||
it does basic validation whether the specified exchange and pairs are valid.
|
it does basic validation whether the specified exchange and pairs are valid.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._api: ccxt.Exchange = None
|
self._api: ccxt.Exchange
|
||||||
self._api_async: ccxt_async.Exchange = None
|
self._api_async: ccxt_async.Exchange
|
||||||
self._markets: Dict = {}
|
self._markets: Dict = {}
|
||||||
self._trading_fees: Dict[str, Any] = {}
|
self._trading_fees: Dict[str, Any] = {}
|
||||||
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
||||||
@ -291,7 +291,7 @@ class Exchange:
|
|||||||
return self._markets
|
return self._markets
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def precisionMode(self) -> str:
|
def precisionMode(self) -> int:
|
||||||
"""exchange ccxt precisionMode"""
|
"""exchange ccxt precisionMode"""
|
||||||
return self._api.precisionMode
|
return self._api.precisionMode
|
||||||
|
|
||||||
@ -322,7 +322,7 @@ class Exchange:
|
|||||||
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
|
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
|
||||||
timeframe, self._ft_has.get('ohlcv_candle_limit')))
|
timeframe, self._ft_has.get('ohlcv_candle_limit')))
|
||||||
|
|
||||||
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
|
def get_markets(self, base_currencies: List[str] = [], quote_currencies: List[str] = [],
|
||||||
spot_only: bool = False, margin_only: bool = False, futures_only: bool = False,
|
spot_only: bool = False, margin_only: bool = False, futures_only: bool = False,
|
||||||
tradable_only: bool = True,
|
tradable_only: bool = True,
|
||||||
active_only: bool = False) -> Dict[str, Any]:
|
active_only: bool = False) -> Dict[str, Any]:
|
||||||
@ -1164,7 +1164,7 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
def fetch_order(self, order_id: str, pair: str, params={}) -> Dict:
|
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return self.fetch_dry_run_order(order_id)
|
return self.fetch_dry_run_order(order_id)
|
||||||
try:
|
try:
|
||||||
@ -1186,8 +1186,8 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
fetch_stoploss_order = fetch_order
|
return self.fetch_order(order_id, pair, params)
|
||||||
|
|
||||||
def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
|
def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
|
||||||
stoploss_order: bool = False) -> Dict:
|
stoploss_order: bool = False) -> Dict:
|
||||||
@ -1212,7 +1212,7 @@ class Exchange:
|
|||||||
and order.get('filled') == 0.0)
|
and order.get('filled') == 0.0)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def cancel_order(self, order_id: str, pair: str, params={}) -> Dict:
|
def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
try:
|
try:
|
||||||
order = self.fetch_dry_run_order(order_id)
|
order = self.fetch_dry_run_order(order_id)
|
||||||
@ -1238,8 +1238,8 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
# Assign method to cancel_stoploss_order to allow easy overriding in other classes
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
cancel_stoploss_order = cancel_order
|
return self.cancel_order(order_id, pair, params)
|
||||||
|
|
||||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||||
if not isinstance(corder, dict):
|
if not isinstance(corder, dict):
|
||||||
@ -1718,7 +1718,7 @@ class Exchange:
|
|||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, candle_type: CandleType,
|
since_ms: int, candle_type: CandleType,
|
||||||
is_new_pair: bool = False, raise_: bool = False,
|
is_new_pair: bool = False, raise_: bool = False,
|
||||||
until_ms: int = None
|
until_ms: Optional[int] = None
|
||||||
) -> Tuple[str, str, str, List]:
|
) -> Tuple[str, str, str, List]:
|
||||||
"""
|
"""
|
||||||
Download historic ohlcv
|
Download historic ohlcv
|
||||||
@ -1779,7 +1779,7 @@ class Exchange:
|
|||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||||
since_ms: Optional[int] = None, cache: bool = True,
|
since_ms: Optional[int] = None, cache: bool = True,
|
||||||
drop_incomplete: bool = None
|
drop_incomplete: Optional[bool] = None
|
||||||
) -> Dict[PairWithTimeframe, DataFrame]:
|
) -> Dict[PairWithTimeframe, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Refresh in-memory OHLCV asynchronously and set `_klines` with the result
|
Refresh in-memory OHLCV asynchronously and set `_klines` with the result
|
||||||
|
@ -104,7 +104,7 @@ class Ftx(Exchange):
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return self.fetch_dry_run_order(order_id)
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ class Ftx(Exchange):
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
|
@ -71,14 +71,14 @@ class Gateio(Exchange):
|
|||||||
}
|
}
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
return self.fetch_order(
|
return self.fetch_order(
|
||||||
order_id=order_id,
|
order_id=order_id,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
params={'stop': True}
|
params={'stop': True}
|
||||||
)
|
)
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
return self.cancel_order(
|
return self.cancel_order(
|
||||||
order_id=order_id,
|
order_id=order_id,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
|
@ -500,7 +500,8 @@ class Backtesting:
|
|||||||
stake_available = self.wallets.get_available_stake_amount()
|
stake_available = self.wallets.get_available_stake_amount()
|
||||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||||
default_retval=None)(
|
default_retval=None)(
|
||||||
trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
trade=trade, # type: ignore[arg-type]
|
||||||
|
current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||||
current_profit=current_profit, min_stake=min_stake,
|
current_profit=current_profit, min_stake=min_stake,
|
||||||
max_stake=min(max_stake, stake_available))
|
max_stake=min(max_stake, stake_available))
|
||||||
|
|
||||||
@ -574,7 +575,8 @@ class Backtesting:
|
|||||||
if order_type == 'limit':
|
if order_type == 'limit':
|
||||||
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
default_retval=close_rate)(
|
default_retval=close_rate)(
|
||||||
pair=trade.pair, trade=trade,
|
pair=trade.pair,
|
||||||
|
trade=trade, # type: ignore[arg-type]
|
||||||
current_time=exit_candle_time,
|
current_time=exit_candle_time,
|
||||||
proposed_rate=close_rate, current_profit=current_profit,
|
proposed_rate=close_rate, current_profit=current_profit,
|
||||||
exit_tag=exit_reason)
|
exit_tag=exit_reason)
|
||||||
@ -588,7 +590,10 @@ class Backtesting:
|
|||||||
time_in_force = self.strategy.order_time_in_force['exit']
|
time_in_force = self.strategy.order_time_in_force['exit']
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
pair=trade.pair,
|
||||||
|
trade=trade, # type: ignore[arg-type]
|
||||||
|
order_type='limit',
|
||||||
|
amount=trade.amount,
|
||||||
rate=close_rate,
|
rate=close_rate,
|
||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
sell_reason=exit_reason, # deprecated
|
sell_reason=exit_reason, # deprecated
|
||||||
@ -664,7 +669,7 @@ class Backtesting:
|
|||||||
return self._get_exit_trade_entry_for_candle(trade, row)
|
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||||
|
|
||||||
def get_valid_price_and_stake(
|
def get_valid_price_and_stake(
|
||||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
||||||
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
||||||
trade: Optional[LocalTrade], order_type: str
|
trade: Optional[LocalTrade], order_type: str
|
||||||
) -> Tuple[float, float, float, float]:
|
) -> Tuple[float, float, float, float]:
|
||||||
@ -738,8 +743,9 @@ class Backtesting:
|
|||||||
order_type = self.strategy.order_types['entry']
|
order_type = self.strategy.order_types['entry']
|
||||||
pos_adjust = trade is not None and requested_rate is None
|
pos_adjust = trade is not None and requested_rate is None
|
||||||
|
|
||||||
|
stake_amount_ = stake_amount or (trade.stake_amount if trade else 0.0)
|
||||||
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
|
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,
|
pair, row, row[OPEN_IDX], stake_amount_, direction, current_time, entry_tag, trade,
|
||||||
order_type
|
order_type
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -909,7 +915,9 @@ class Backtesting:
|
|||||||
Check if current analyzed order has to be canceled.
|
Check if current analyzed order has to be canceled.
|
||||||
Returns True if the trade should be Deleted (initial order was canceled).
|
Returns True if the trade should be Deleted (initial order was canceled).
|
||||||
"""
|
"""
|
||||||
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
|
timedout = self.strategy.ft_check_timed_out(
|
||||||
|
trade, # type: ignore[arg-type]
|
||||||
|
order, current_time)
|
||||||
if timedout:
|
if timedout:
|
||||||
if order.side == trade.entry_side:
|
if order.side == trade.entry_side:
|
||||||
self.timedout_entry_orders += 1
|
self.timedout_entry_orders += 1
|
||||||
@ -938,7 +946,8 @@ class Backtesting:
|
|||||||
if order.side == trade.entry_side and current_time > order.order_date_utc:
|
if order.side == trade.entry_side and current_time > order.order_date_utc:
|
||||||
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
|
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
|
||||||
default_retval=order.price)(
|
default_retval=order.price)(
|
||||||
trade=trade, order=order, pair=trade.pair, current_time=current_time,
|
trade=trade, # type: ignore[arg-type]
|
||||||
|
order=order, pair=trade.pair, current_time=current_time,
|
||||||
proposed_rate=row[OPEN_IDX], current_order_rate=order.price,
|
proposed_rate=row[OPEN_IDX], current_order_rate=order.price,
|
||||||
entry_tag=trade.enter_tag, side=trade.trade_direction
|
entry_tag=trade.enter_tag, side=trade.trade_direction
|
||||||
) # default value is current order price
|
) # default value is current order price
|
||||||
|
@ -16,7 +16,7 @@ from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirecti
|
|||||||
SignalType, TradingMode)
|
SignalType, TradingMode)
|
||||||
from freqtrade.exceptions import OperationalException, StrategyError
|
from freqtrade.exceptions import OperationalException, StrategyError
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
||||||
from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
|
from freqtrade.persistence import Order, PairLocks, Trade
|
||||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||||
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||||
_create_and_merge_informative_pair,
|
_create_and_merge_informative_pair,
|
||||||
@ -429,7 +429,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
|
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
|
||||||
|
|
||||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Customize stake size for each new trade.
|
Customize stake size for each new trade.
|
||||||
@ -447,8 +447,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
return proposed_stake
|
return proposed_stake
|
||||||
|
|
||||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||||
current_rate: float, current_profit: float, min_stake: float,
|
current_rate: float, current_profit: float,
|
||||||
max_stake: float, **kwargs) -> Optional[float]:
|
min_stake: Optional[float], max_stake: float,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||||
This means extra buy orders with additional fees.
|
This means extra buy orders with additional fees.
|
||||||
@ -917,19 +918,20 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
if exit_ and not enter:
|
if exit_ and not enter:
|
||||||
exit_signal = ExitType.EXIT_SIGNAL
|
exit_signal = ExitType.EXIT_SIGNAL
|
||||||
else:
|
else:
|
||||||
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
reason_cust = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
||||||
pair=trade.pair, trade=trade, current_time=current_time,
|
pair=trade.pair, trade=trade, current_time=current_time,
|
||||||
current_rate=current_rate, current_profit=current_profit)
|
current_rate=current_rate, current_profit=current_profit)
|
||||||
if custom_reason:
|
if reason_cust:
|
||||||
exit_signal = ExitType.CUSTOM_EXIT
|
exit_signal = ExitType.CUSTOM_EXIT
|
||||||
if isinstance(custom_reason, str):
|
if isinstance(reason_cust, str):
|
||||||
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
|
custom_reason = reason_cust
|
||||||
|
if len(reason_cust) > CUSTOM_EXIT_MAX_LENGTH:
|
||||||
logger.warning(f'Custom exit reason returned from '
|
logger.warning(f'Custom exit reason returned from '
|
||||||
f'custom_exit is too long and was trimmed'
|
f'custom_exit is too long and was trimmed'
|
||||||
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
|
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
|
||||||
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
|
custom_reason = reason_cust[:CUSTOM_EXIT_MAX_LENGTH]
|
||||||
else:
|
else:
|
||||||
custom_reason = None
|
custom_reason = ''
|
||||||
if (
|
if (
|
||||||
exit_signal == ExitType.CUSTOM_EXIT
|
exit_signal == ExitType.CUSTOM_EXIT
|
||||||
or (exit_signal == ExitType.EXIT_SIGNAL
|
or (exit_signal == ExitType.EXIT_SIGNAL
|
||||||
@ -1075,7 +1077,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
else:
|
else:
|
||||||
return current_profit > roi
|
return current_profit > roi
|
||||||
|
|
||||||
def ft_check_timed_out(self, trade: LocalTrade, order: Order,
|
def ft_check_timed_out(self, trade: Trade, order: Order,
|
||||||
current_time: datetime) -> bool:
|
current_time: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
FT Internal method.
|
FT Internal method.
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Callable, TypeVar, cast
|
||||||
|
|
||||||
from freqtrade.exceptions import StrategyError
|
from freqtrade.exceptions import StrategyError
|
||||||
|
|
||||||
@ -7,12 +9,16 @@ from freqtrade.exceptions import StrategyError
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_error=False):
|
F = TypeVar('F', bound=Callable[..., Any])
|
||||||
|
|
||||||
|
|
||||||
|
def strategy_safe_wrapper(f: F, message: str = "", default_retval=None, supress_error=False) -> F:
|
||||||
"""
|
"""
|
||||||
Wrapper around user-provided methods and functions.
|
Wrapper around user-provided methods and functions.
|
||||||
Caches all exceptions and returns either the default_retval (if it's not None) or raises
|
Caches all exceptions and returns either the default_retval (if it's not None) or raises
|
||||||
a StrategyError exception, which then needs to be handled by the calling method.
|
a StrategyError exception, which then needs to be handled by the calling method.
|
||||||
"""
|
"""
|
||||||
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
if 'trade' in kwargs:
|
if 'trade' in kwargs:
|
||||||
@ -37,4 +43,4 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_err
|
|||||||
raise StrategyError(str(error)) from error
|
raise StrategyError(str(error)) from error
|
||||||
return default_retval
|
return default_retval
|
||||||
|
|
||||||
return wrapper
|
return cast(F, wrapper)
|
||||||
|
@ -6,7 +6,7 @@ import numpy as np # noqa
|
|||||||
import pandas as pd # noqa
|
import pandas as pd # noqa
|
||||||
from pandas import DataFrame # noqa
|
from pandas import DataFrame # noqa
|
||||||
from datetime import datetime # noqa
|
from datetime import datetime # noqa
|
||||||
from typing import Optional # noqa
|
from typing import Optional, Union # noqa
|
||||||
|
|
||||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
IStrategy, IntParameter)
|
IStrategy, IntParameter)
|
||||||
|
@ -13,7 +13,7 @@ def bot_loop_start(self, **kwargs) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float,
|
def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float,
|
||||||
entry_tag: Optional[str], **kwargs) -> float:
|
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Custom entry price logic, returning the new entry price.
|
Custom entry price logic, returning the new entry price.
|
||||||
|
|
||||||
@ -80,8 +80,8 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
|
|||||||
return proposed_rate
|
return proposed_rate
|
||||||
|
|
||||||
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||||
side: str, entry_tag: Optional[str], **kwargs) -> float:
|
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Customize stake size for each new trade.
|
Customize stake size for each new trade.
|
||||||
|
|
||||||
@ -244,8 +244,8 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
||||||
current_rate: float, current_profit: float, min_stake: float,
|
current_rate: float, current_profit: float, min_stake: Optional[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.
|
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||||
This means extra buy orders with additional fees.
|
This means extra buy orders with additional fees.
|
||||||
|
@ -28,6 +28,17 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
|
|||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
exclude = [
|
||||||
|
'^build_helpers\.py$'
|
||||||
|
]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "tests.*"
|
||||||
|
ignore_errors = true
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools >= 46.4.0", "wheel"]
|
requires = ["setuptools >= 46.4.0", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
10
setup.cfg
10
setup.cfg
@ -50,13 +50,3 @@ exclude =
|
|||||||
.eggs,
|
.eggs,
|
||||||
user_data,
|
user_data,
|
||||||
|
|
||||||
[mypy]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
warn_unused_ignores = True
|
|
||||||
exclude = (?x)(
|
|
||||||
^build_helpers\.py$
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
[mypy-tests.*]
|
|
||||||
ignore_errors = True
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -151,7 +152,8 @@ class StrategyTestV2(IStrategy):
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float,
|
def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
current_profit: float, min_stake: float, max_stake: float, **kwargs):
|
current_profit: float,
|
||||||
|
min_stake: Optional[float], max_stake: float, **kwargs):
|
||||||
|
|
||||||
if current_profit < -0.0075:
|
if current_profit < -0.0075:
|
||||||
orders = trade.select_filled_orders('buy')
|
orders = trade.select_filled_orders('buy')
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -185,7 +186,8 @@ class StrategyTestV3(IStrategy):
|
|||||||
return 3.0
|
return 3.0
|
||||||
|
|
||||||
def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float,
|
def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
current_profit: float, min_stake: float, max_stake: float, **kwargs):
|
current_profit: float,
|
||||||
|
min_stake: Optional[float], max_stake: float, **kwargs):
|
||||||
|
|
||||||
if current_profit < -0.0075:
|
if current_profit < -0.0075:
|
||||||
orders = trade.select_filled_orders(trade.entry_side)
|
orders = trade.select_filled_orders(trade.entry_side)
|
||||||
|
Loading…
Reference in New Issue
Block a user