Merge branch 'freqtrade:develop' into pos_adjust

This commit is contained in:
Stefano Ariestasia
2022-01-25 10:30:18 +09:00
committed by GitHub
18 changed files with 222 additions and 93 deletions

View File

@@ -614,7 +614,7 @@ class Exchange:
'side': side,
'filled': 0,
'remaining': _amount,
'datetime': arrow.utcnow().isoformat(),
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" else "open",
'fee': None,

View File

@@ -9,8 +9,6 @@ from math import isclose
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple
import arrow
from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe
@@ -540,7 +538,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None
enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade)
pair, price, stake_amount, buy_tag, trade)
if not stake_amount:
return False
@@ -559,7 +557,8 @@ class FreqtradeBot(LoggingMixin):
if not pos_adjust and not strategy_safe_wrapper(
self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
entry_tag=buy_tag):
logger.info(f"User requested abortion of buying {pair}")
return False
amount = self.exchange.amount_to_precision(pair, amount)
@@ -669,6 +668,7 @@ class FreqtradeBot(LoggingMixin):
def get_valid_enter_price_and_stake(
self, pair: str, price: Optional[float], stake_amount: float,
entry_tag: Optional[str],
trade: Optional[Trade]) -> Tuple[float, float]:
if price:
enter_limit_requested = price
@@ -678,7 +678,7 @@ class FreqtradeBot(LoggingMixin):
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc),
proposed_rate=proposed_enter_rate)
proposed_rate=proposed_enter_rate, entry_tag=entry_tag)
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
if not enter_limit_requested:
@@ -691,7 +691,7 @@ class FreqtradeBot(LoggingMixin):
default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=max_stake_amount)
min_stake=min_stake_amount, max_stake=max_stake_amount, entry_tag=entry_tag)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
return enter_limit_requested, stake_amount
@@ -966,20 +966,6 @@ class FreqtradeBot(LoggingMixin):
return True
return False
def _check_timed_out(self, side: str, order: dict) -> bool:
"""
Check if timeout is active, and if the order is still open and timed out
"""
timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None:
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime
return (order['status'] == 'open' and order['side'] == side
and ordertime < timeout_threshold)
return False
def check_handle_timedout(self) -> None:
"""
Check if any orders are timed out and cancel if necessary
@@ -1000,20 +986,16 @@ class FreqtradeBot(LoggingMixin):
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled
or self._check_timed_out('buy', order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
or self.strategy.ft_check_timed_out(
'buy', trade, order, datetime.now(timezone.utc))
)):
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled
or self._check_timed_out('sell', order)
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
or self.strategy.ft_check_timed_out(
'sell', trade, order, datetime.now(timezone.utc)))
):
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)

View File

@@ -468,11 +468,13 @@ class Backtesting:
def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(),
proposed_rate=row[OPEN_IDX]) # default value is the open rate
pair=pair, current_time=current_time,
proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate
# Move rate to within the candle's low/high rate
propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX])
@@ -489,8 +491,9 @@ class Backtesting:
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
pair=pair, current_time=current_time, current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
entry_tag=entry_tag)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
@@ -505,27 +508,28 @@ class Backtesting:
if not pos_adjust:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
time_in_force=time_in_force, current_time=current_time,
entry_tag=entry_tag):
return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
amount = round(stake_amount / propose_rate, 8)
if trade is None:
# Enter trade
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
trade = LocalTrade(
pair=pair,
open_rate=propose_rate,
open_date=row[DATE_IDX].to_pydatetime(),
open_date=current_time,
stake_amount=stake_amount,
amount=amount,
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
buy_tag=entry_tag,
exchange='backtesting',
orders=[]
)
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
order = Order(
ft_is_open=False,
@@ -535,6 +539,9 @@ class Backtesting:
side="buy",
order_type="market",
status="closed",
order_date=current_time,
order_filled_date=current_time,
order_update_date=current_time,
price=propose_rate,
average=propose_rate,
amount=amount,

View File

@@ -577,18 +577,19 @@ class LocalTrade():
total_amount = 0.0
total_stake = 0.0
for temp_order in self.orders:
if (temp_order.ft_is_open or
(temp_order.ft_order_side != 'buy') or
(temp_order.status not in NON_OPEN_EXCHANGE_STATES)):
for o in self.orders:
if (o.ft_is_open or
(o.ft_order_side != 'buy') or
(o.status not in NON_OPEN_EXCHANGE_STATES)):
continue
tmp_amount = temp_order.amount
if temp_order.filled is not None:
tmp_amount = temp_order.filled
if tmp_amount > 0.0 and temp_order.average is not None:
tmp_amount = o.amount
tmp_price = o.average or o.price
if o.filled is not None:
tmp_amount = o.filled
if tmp_amount > 0.0 and tmp_price is not None:
total_amount += tmp_amount
total_stake += temp_order.average * tmp_amount
total_stake += tmp_price * tmp_amount
if total_amount > 0:
self.open_rate = total_stake / total_amount

View File

@@ -279,6 +279,7 @@ class ForceBuyPayload(BaseModel):
pair: str
price: Optional[float]
ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float]
class ForceSellPayload(BaseModel):

View File

@@ -31,7 +31,8 @@ logger = logging.getLogger(__name__)
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
# 1.11: forcebuy and forcesell accept ordertype
# 1.12: add blacklist delete endpoint
API_VERSION = 1.12
# 1.13: forcebuy supports stake_amount
API_VERSION = 1.13
# Public API, requires no auth.
router_public = APIRouter()
@@ -134,7 +135,9 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype)
stake_amount = payload.stakeamount if payload.stakeamount else None
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount)
if trade:
return ForceBuyResponse.parse_obj(trade.to_json())

View File

@@ -712,8 +712,8 @@ class RPC:
self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'}
def _rpc_forcebuy(self, pair: str, price: Optional[float],
order_type: Optional[str] = None) -> Optional[Trade]:
def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
stake_amount: Optional[float] = None) -> Optional[Trade]:
"""
Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price
@@ -738,14 +738,15 @@ class RPC:
if not self._freqtrade.strategy.position_adjustment_enable:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
if not stake_amount:
# gen stake amount
stake_amount = self._freqtrade.wallets.get_trade_stake_amount(pair)
# execute buy
if not order_type:
order_type = self._freqtrade.strategy.order_types.get(
'forcebuy', self._freqtrade.strategy.order_types['buy'])
if self._freqtrade.execute_entry(pair, stakeamount, price,
if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade):
Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()

View File

@@ -189,7 +189,17 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return dataframe
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
Check buy timeout function callback.
This method can be used to override the buy-timeout.
@@ -202,12 +212,14 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair the trade is for
:param trade: trade object.
:param order: Order dictionary as returned from CCXT.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is cancelled.
"""
return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
Check sell timeout function callback.
This method can be used to override the sell-timeout.
@@ -220,22 +232,15 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair the trade is for
:param trade: trade object.
:param order: Order dictionary as returned from CCXT.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is cancelled.
"""
return False
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool:
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
**kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
@@ -251,6 +256,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@@ -308,7 +314,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return self.stoploss
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
**kwargs) -> float:
entry_tag: Optional[str], **kwargs) -> float:
"""
Custom entry price logic, returning the new entry price.
@@ -319,6 +325,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided
"""
@@ -370,7 +377,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
entry_tag: Optional[str], **kwargs) -> float:
"""
Customize stake size for each new trade. This method is not called when edge module is
enabled.
@@ -381,6 +388,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:return: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake
@@ -391,6 +399,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
This means extra buy orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
@@ -853,6 +862,29 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
return current_profit > roi
def ft_check_timed_out(self, side: str, trade: Trade, order: Dict,
current_time: datetime) -> bool:
"""
FT Internal method.
Check if timeout is active, and if the order is still open and timed out
"""
timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None:
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = current_time + timedelta(**timeout_kwargs)
timedout = (order['status'] == 'open' and order['side'] == side
and ordertime < timeout_threshold)
if timedout:
return True
time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout
return strategy_safe_wrapper(time_method,
default_retval=False)(
pair=trade.pair, trade=trade, order=order,
current_time=current_time)
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
"""
Populates indicators for given candle (OHLCV) data (for multiple pairs)

View File

@@ -12,9 +12,47 @@ 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:
"""
Custom entry price logic, returning the new entry price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set entry price
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
: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 proposed_rate
def custom_exit_price(self, pair: str, trade: 'Trade',
current_time: 'datetime', proposed_rate: float,
current_profit: float, **kwargs) -> float:
"""
Custom exit price logic, returning the new exit price.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None, orderbook is used to set exit price
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New exit price value if provided
"""
return proposed_rate
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
entry_tag: 'Optional[str]', **kwargs) -> float:
"""
Customize stake size for each new trade. This method is not called when edge module is
enabled.
@@ -25,6 +63,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:return: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake
@@ -78,7 +117,8 @@ def custom_sell(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', **kwargs) -> bool:
time_in_force: str, current_time: 'datetime', entry_tag: 'Optional[str]',
**kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
@@ -94,6 +134,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@@ -167,3 +208,26 @@ def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
:return bool: When True is returned, then the sell-order is cancelled.
"""
return False
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]':
"""
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
This means extra buy orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade
"""
return None