Merge branch 'freqtrade:develop' into fix-docs

This commit is contained in:
Stefano Ariestasia 2022-01-25 10:30:03 +09:00 committed by GitHub
commit 7c975df42a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 255 additions and 99 deletions

View File

@ -1,4 +1,4 @@
mkdocs==1.2.3 mkdocs==1.2.3
mkdocs-material==8.1.7 mkdocs-material==8.1.8
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.1 pymdown-extensions==9.1

View File

@ -362,8 +362,8 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def custom_entry_price(self, pair: str, current_time: datetime, def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
proposed_rate, **kwargs) -> float: entry_tag: Optional[str], **kwargs) -> float:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe) timeframe=self.timeframe)
@ -413,7 +413,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
The function must return either `True` (cancel order) or `False` (keep order alive). The function must return either `True` (cancel order) or `False` (keep order alive).
``` python ``` python
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -426,22 +426,24 @@ class AwesomeStrategy(IStrategy):
'sell': 60 * 25 'sell': 60 * 25
} }
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
return True return True
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
return True return True
return False 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,
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
return True return True
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
return True return True
return False return False
``` ```
@ -500,7 +502,8 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, 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. Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -618,7 +621,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: float, max_stake: float,
**kwargs) -> float: entry_tag: Optional[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
# This also applies to fixed stakes # This also applies to fixed stakes

View File

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

View File

@ -9,8 +9,6 @@ from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import arrow
from freqtrade import __version__, constants from freqtrade import __version__, constants
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
@ -533,7 +531,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake( 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: if not stake_amount:
return False return False
@ -552,7 +550,8 @@ class FreqtradeBot(LoggingMixin):
if not pos_adjust and not strategy_safe_wrapper( if not pos_adjust and not strategy_safe_wrapper(
self.strategy.confirm_trade_entry, default_retval=True)( self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=enter_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)): 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}") logger.info(f"User requested abortion of buying {pair}")
return False return False
amount = self.exchange.amount_to_precision(pair, amount) amount = self.exchange.amount_to_precision(pair, amount)
@ -662,6 +661,7 @@ class FreqtradeBot(LoggingMixin):
def get_valid_enter_price_and_stake( def get_valid_enter_price_and_stake(
self, pair: str, price: Optional[float], stake_amount: float, self, pair: str, price: Optional[float], stake_amount: float,
entry_tag: Optional[str],
trade: Optional[Trade]) -> Tuple[float, float]: trade: Optional[Trade]) -> Tuple[float, float]:
if price: if price:
enter_limit_requested = price enter_limit_requested = price
@ -671,7 +671,7 @@ class FreqtradeBot(LoggingMixin):
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=proposed_enter_rate)(
pair=pair, current_time=datetime.now(timezone.utc), 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) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
if not enter_limit_requested: if not enter_limit_requested:
@ -684,7 +684,7 @@ class FreqtradeBot(LoggingMixin):
default_retval=stake_amount)( default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_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) 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) stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
return enter_limit_requested, stake_amount return enter_limit_requested, stake_amount
@ -959,20 +959,6 @@ class FreqtradeBot(LoggingMixin):
return True return True
return False 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: def check_handle_timedout(self) -> None:
""" """
Check if any orders are timed out and cancel if necessary Check if any orders are timed out and cancel if necessary
@ -993,20 +979,16 @@ class FreqtradeBot(LoggingMixin):
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self._check_timed_out('buy', order) or self.strategy.ft_check_timed_out(
or strategy_safe_wrapper(self.strategy.check_buy_timeout, 'buy', trade, order, datetime.now(timezone.utc))
default_retval=False)(pair=trade.pair, )):
trade=trade,
order=order))):
self.handle_cancel_enter(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 ( elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self._check_timed_out('sell', order) or self.strategy.ft_check_timed_out(
or strategy_safe_wrapper(self.strategy.check_sell_timeout, 'sell', trade, order, datetime.now(timezone.utc)))
default_retval=False)(pair=trade.pair, ):
trade=trade,
order=order))):
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)

View File

@ -248,8 +248,10 @@ def get_strategy_run_id(strategy) -> str:
if k in config: if k in config:
del config[k] del config[k]
# Explicitly allow NaN values (e.g. max_open_trades).
# as it does not matter for getting the hash.
digest.update(rapidjson.dumps(config, default=str, digest.update(rapidjson.dumps(config, default=str,
number_mode=rapidjson.NM_NATIVE).encode('utf-8')) number_mode=rapidjson.NM_NAN).encode('utf-8'))
with open(strategy.__file__, 'rb') as fp: with open(strategy.__file__, 'rb') as fp:
digest.update(fp.read()) digest.update(fp.read())
return digest.hexdigest().lower() return digest.hexdigest().lower()

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
Stats, StatusMsg, StrategyListResponse, Stats, StatusMsg, StrategyListResponse,
StrategyResponse, SysInfo, Version, StrategyResponse, SysInfo, Version,
WhitelistResponse) WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -31,7 +31,8 @@ logger = logging.getLogger(__name__)
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen. # Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
# 1.11: forcebuy and forcesell accept ordertype # 1.11: forcebuy and forcesell accept ordertype
# 1.12: add blacklist delete endpoint # 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. # Public API, requires no auth.
router_public = APIRouter() 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']) @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None 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: if trade:
return ForceBuyResponse.parse_obj(trade.to_json()) return ForceBuyResponse.parse_obj(trade.to_json())
@ -217,12 +220,14 @@ def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc: RPC = Dep
@router.get('/pair_history', response_model=PairHistory, tags=['candle data']) @router.get('/pair_history', response_model=PairHistory, tags=['candle data'])
def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
config=Depends(get_config)): config=Depends(get_config), exchange=Depends(get_exchange)):
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config = deepcopy(config) config = deepcopy(config)
config.update({ config.update({
'strategy': strategy, 'strategy': strategy,
}) })
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange, exchange)
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) @router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])

View File

@ -1,5 +1,7 @@
from typing import Any, Dict, Iterator, Optional from typing import Any, Dict, Iterator, Optional
from fastapi import Depends
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
@ -28,3 +30,11 @@ def get_config() -> Dict[str, Any]:
def get_api_config() -> Dict[str, Any]: def get_api_config() -> Dict[str, Any]:
return ApiServer._config['api_server'] return ApiServer._config['api_server']
def get_exchange(config=Depends(get_config)):
if not ApiServer._exchange:
from freqtrade.resolvers import ExchangeResolver
ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config)
return ApiServer._exchange

View File

@ -41,6 +41,8 @@ class ApiServer(RPCHandler):
_has_rpc: bool = False _has_rpc: bool = False
_bgtask_running: bool = False _bgtask_running: bool = False
_config: Dict[str, Any] = {} _config: Dict[str, Any] = {}
# Exchange - only available in webserver mode.
_exchange = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
""" """

View File

@ -709,8 +709,8 @@ class RPC:
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'} return {'result': f'Created sell order for trade {trade_id}.'}
def _rpc_forcebuy(self, pair: str, price: Optional[float], def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
order_type: Optional[str] = None) -> Optional[Trade]: stake_amount: Optional[float] = None) -> Optional[Trade]:
""" """
Handler for forcebuy <asset> <price> Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
@ -735,14 +735,15 @@ class RPC:
if not self._freqtrade.strategy.position_adjustment_enable: if not self._freqtrade.strategy.position_adjustment_enable:
raise RPCException(f'position for {pair} already open - id: {trade.id}') raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount if not stake_amount:
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # gen stake amount
stake_amount = self._freqtrade.wallets.get_trade_stake_amount(pair)
# execute buy # execute buy
if not order_type: if not order_type:
order_type = self._freqtrade.strategy.order_types.get( order_type = self._freqtrade.strategy.order_types.get(
'forcebuy', self._freqtrade.strategy.order_types['buy']) '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): ordertype=order_type, trade=trade):
Trade.commit() Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
@ -997,7 +998,7 @@ class RPC:
@staticmethod @staticmethod
def _rpc_analysed_history_full(config, pair: str, timeframe: str, def _rpc_analysed_history_full(config, pair: str, timeframe: str,
timerange: str) -> Dict[str, Any]: timerange: str, exchange) -> Dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(timerange) timerange_parsed = TimeRange.parse_timerange(timerange)
_data = load_data( _data = load_data(
@ -1012,7 +1013,7 @@ class RPC:
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
strategy.dp = DataProvider(config, exchange=None, pairlists=None) strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair})

View File

@ -188,7 +188,17 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return dataframe 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. Check buy timeout function callback.
This method can be used to override the buy-timeout. This method can be used to override the buy-timeout.
@ -201,12 +211,14 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :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. :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 bool: When True is returned, then the buy-order is cancelled.
""" """
return False 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. Check sell timeout function callback.
This method can be used to override the sell-timeout. This method can be used to override the sell-timeout.
@ -219,22 +231,15 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :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. :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 bool: When True is returned, then the sell-order is cancelled.
""" """
return False 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, 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. Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -250,6 +255,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param rate: Rate that's going to be used when using limit orders :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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :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. :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. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process False aborts the process
@ -307,7 +313,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return self.stoploss return self.stoploss
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,
**kwargs) -> float: entry_tag: Optional[str], **kwargs) -> float:
""" """
Custom entry price logic, returning the new entry price. Custom entry price logic, returning the new entry price.
@ -318,6 +324,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. :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. :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 float: New entry price value if provided
""" """
@ -369,7 +376,7 @@ class IStrategy(ABC, HyperStrategyMixin):
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: 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 Customize stake size for each new trade. This method is not called when edge module is
enabled. enabled.
@ -380,6 +387,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param proposed_stake: A stake amount proposed by the bot. :param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :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: A stake size, which is between min_stake and max_stake.
""" """
return proposed_stake return proposed_stake
@ -390,6 +398,7 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
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.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
@ -852,6 +861,29 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return current_profit > roi 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]: def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Populates indicators for given candle (OHLCV) data (for multiple pairs) Populates indicators for given candle (OHLCV) data (for multiple pairs)

View File

@ -12,9 +12,47 @@ def bot_loop_start(self, **kwargs) -> None:
""" """
pass 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, 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: 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 Customize stake size for each new trade. This method is not called when edge module is
enabled. 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 proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :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: A stake size, which is between min_stake and max_stake.
""" """
return proposed_stake return proposed_stake
@ -78,7 +117,8 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
return None return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, 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. Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or 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 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 time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :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. :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. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process 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 bool: When True is returned, then the sell-order is cancelled.
""" """
return False 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

View File

@ -8,7 +8,7 @@ flake8==4.0.1
flake8-tidy-imports==4.6.0 flake8-tidy-imports==4.6.0
mypy==0.931 mypy==0.931
pytest==6.2.5 pytest==6.2.5
pytest-asyncio==0.17.1 pytest-asyncio==0.17.2
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-mock==3.6.1 pytest-mock==3.6.1
pytest-random-order==1.0.4 pytest-random-order==1.0.4
@ -21,9 +21,9 @@ nbconvert==6.4.0
# mypy types # mypy types
types-cachetools==4.2.9 types-cachetools==4.2.9
types-filelock==3.2.4 types-filelock==3.2.5
types-requests==2.27.7 types-requests==2.27.7
types-tabulate==0.8.5 types-tabulate==0.8.5
# Extensions to datetime library # Extensions to datetime library
types-python-dateutil==2.8.7 types-python-dateutil==2.8.8

View File

@ -7,7 +7,7 @@ ccxt==1.68.20
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.1 cryptography==36.0.1
aiohttp==3.8.1 aiohttp==3.8.1
SQLAlchemy==1.4.29 SQLAlchemy==1.4.31
python-telegram-bot==13.10 python-telegram-bot==13.10
arrow==1.2.1 arrow==1.2.1
cachetools==4.2.2 cachetools==4.2.2
@ -32,7 +32,7 @@ python-rapidjson==1.5
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.72.0 fastapi==0.73.0
uvicorn==0.17.0 uvicorn==0.17.0
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0

View File

@ -21,6 +21,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.enums import RunMode, SellType from freqtrade.enums import RunMode, SellType
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.misc import get_strategy_run_id
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade from freqtrade.persistence import LocalTrade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
@ -762,6 +763,8 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
# While buy-signals are unrealistic, running backtesting # While buy-signals are unrealistic, running backtesting
# over and over again should not cause different results # over and over again should not cause different results
for [contour, numres] in tests: for [contour, numres] in tests:
# Debug output for random test failure
print(f"{contour}, {numres}")
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres
@ -1357,3 +1360,13 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
for line in exists: for line in exists:
assert log_has(line, caplog) assert log_has(line, caplog)
def test_get_strategy_run_id(default_conf_usdt):
default_conf_usdt.update({
'strategy': 'StrategyTestV2',
'max_open_trades': float('inf')
})
strategy = StrategyResolver.load_strategy(default_conf_usdt)
x = get_strategy_run_id(strategy)
assert isinstance(x, str)

View File

@ -1114,9 +1114,14 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
with pytest.raises(RPCException, with pytest.raises(RPCException,
match=r'Wrong pair selected. Only pairs with stake-currency.*'): match=r'Wrong pair selected. Only pairs with stake-currency.*'):
rpc._rpc_forcebuy('LTC/ETH', 0.0001) rpc._rpc_forcebuy('LTC/ETH', 0.0001)
pair = 'XRP/BTC'
# Test with defined stake_amount
pair = 'LTC/BTC'
trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit', stake_amount=0.05)
assert trade.stake_amount == 0.05
# Test not buying # Test not buying
pair = 'XRP/BTC'
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
freqtradebot.config['stake_amount'] = 0 freqtradebot.config['stake_amount'] = 0
patch_get_signal(freqtradebot) patch_get_signal(freqtradebot)

View File

@ -37,7 +37,7 @@ def test_strategy_test_v2(result, fee):
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', rate=20000, time_in_force='gtc',
current_time=datetime.utcnow()) is True current_time=datetime.utcnow(), entry_tag=None) is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', sell_reason='roi', rate=20000, time_in_force='gtc', sell_reason='roi',
current_time=datetime.utcnow()) is True current_time=datetime.utcnow()) is True

View File

@ -243,6 +243,8 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 2 assert len(trade.orders) == 2
for o in trade.orders:
assert o.status == "closed"
assert trade.stake_amount == 120 assert trade.stake_amount == 120
# Open-rate averaged between 2.0 and 2.0 * 0.995 # Open-rate averaged between 2.0 and 2.0 * 0.995
@ -258,7 +260,6 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid'] assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid']
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_buys == 2 assert trade.nr_of_successful_buys == 2
# Sell # Sell

View File

@ -1663,6 +1663,33 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.fee_open_cost == 2 * o1_fee_cost
assert trade.open_trade_value == 2 * o1_trade_val assert trade.open_trade_value == 2 * o1_trade_val
assert trade.nr_of_successful_buys == 2 assert trade.nr_of_successful_buys == 2
# Check with 1 order
order_noavg = Order(
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
price=o1_rate,
average=None,
filled=o1_amount,
remaining=0,
cost=o1_amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
)
trade.orders.append(order_noavg)
trade.recalc_trade_from_orders()
# Calling recalc with single initial order should not change anything
assert trade.amount == 3 * o1_amount
assert trade.stake_amount == 3 * o1_amount
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == 3 * o1_fee_cost
assert trade.open_trade_value == 3 * o1_trade_val
assert trade.nr_of_successful_buys == 3
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")