Merge branch 'freqtrade:develop' into fix-docs
This commit is contained in:
commit
7c975df42a
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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'])
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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})
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user