Merge branch 'develop' into feat/freqai

This commit is contained in:
Matthias
2022-08-13 08:43:24 +02:00
23 changed files with 219 additions and 154 deletions

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = 'develop'
__version__ = '2022.8.dev'
if 'dev' in __version__:
try:

View File

@@ -4,5 +4,4 @@ from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.configuration.configuration import Configuration
from freqtrade.configuration.PeriodicCache import PeriodicCache
from freqtrade.configuration.timerange import TimeRange

View File

@@ -12,12 +12,12 @@ from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.configuration.PeriodicCache import PeriodicCache
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange, timeframe_to_seconds
from freqtrade.util import PeriodicCache
logger = logging.getLogger(__name__)

View File

@@ -16,7 +16,7 @@ import arrow
import ccxt
import ccxt.async_support as ccxt_async
from cachetools import TTLCache
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_precision
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
@@ -32,6 +32,7 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGE
retrier_async)
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import FtPrecise
CcxtModuleType = Any
@@ -708,10 +709,10 @@ class Exchange:
# counting_mode=self.precisionMode,
# ))
if self.precisionMode == TICK_SIZE:
precision = Precise(str(self.markets[pair]['precision']['price']))
price_str = Precise(str(price))
precision = FtPrecise(self.markets[pair]['precision']['price'])
price_str = FtPrecise(price)
missing = price_str % precision
if not missing == Precise("0"):
if not missing == FtPrecise("0"):
price = round(float(str(price_str - missing + precision)), 14)
else:
symbol_prec = self.markets[pair]['precision']['price']
@@ -849,6 +850,7 @@ class Exchange:
dry_order.update({
'average': average,
'filled': _amount,
'remaining': 0.0,
'cost': (dry_order['amount'] * average) / leverage
})
# market orders will always incurr taker fees

View File

@@ -1,6 +1,6 @@
""" FTX exchange subclass """
import logging
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Optional, Tuple
import ccxt
@@ -116,9 +116,17 @@ class Ftx(Exchange):
if len(order) == 1:
if order[0].get('status') == 'closed':
# Trigger order was triggered ...
real_order_id = order[0].get('info', {}).get('orderId')
real_order_id: Optional[str] = order[0].get('info', {}).get('orderId')
# OrderId may be None for stoploss-market orders
# But contains "average" in these cases.
# So we need to get it through the endpoint
# /conditional_orders/{conditional_order_id}/triggers
if not real_order_id:
res = self._api.privateGetConditionalOrdersConditionalOrderIdTriggers(
params={'conditional_order_id': order_id})
self._log_exchange_response('fetch_stoploss_order2', res)
real_order_id = res['result'][0]['orderId'] if res.get(
'result', []) else None
if real_order_id:
order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1)

View File

@@ -624,7 +624,8 @@ class FreqtradeBot(LoggingMixin):
ordertype: Optional[str] = None,
enter_tag: Optional[str] = None,
trade: Optional[Trade] = None,
order_adjust: bool = False
order_adjust: bool = False,
leverage_: Optional[float] = None,
) -> bool:
"""
Executes a limit buy for the given pair
@@ -640,7 +641,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust)
pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_)
if not stake_amount:
return False
@@ -787,6 +788,7 @@ class FreqtradeBot(LoggingMixin):
entry_tag: Optional[str],
trade: Optional[Trade],
order_adjust: bool,
leverage_: Optional[float],
) -> Tuple[float, float, float]:
if price:
@@ -809,16 +811,19 @@ class FreqtradeBot(LoggingMixin):
if not enter_limit_requested:
raise PricingError('Could not determine entry price.')
if trade is None:
if self.trading_mode != TradingMode.SPOT and trade is None:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side, entry_tag=entry_tag,
) if self.trading_mode != TradingMode.SPOT else 1.0
if leverage_:
leverage = leverage_
else:
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side, entry_tag=entry_tag,
)
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
else:

View File

@@ -8,11 +8,11 @@ from typing import Any, Dict, List, Optional
import arrow
from pandas import DataFrame
from freqtrade.configuration import PeriodicCache
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList
from freqtrade.util import PeriodicCache
logger = logging.getLogger(__name__)

View File

@@ -194,11 +194,11 @@ class OrderSchema(BaseModel):
pair: str
order_id: str
status: str
remaining: float
remaining: Optional[float]
amount: float
safe_price: float
cost: float
filled: float
filled: Optional[float]
ft_order_side: str
order_type: str
is_open: bool
@@ -325,11 +325,13 @@ class ForceEnterPayload(BaseModel):
ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float]
entry_tag: Optional[str]
leverage: Optional[float]
class ForceExitPayload(BaseModel):
tradeid: str
ordertype: Optional[OrderTypeValues]
amount: Optional[float]
class BlacklistPayload(BaseModel):

View File

@@ -37,7 +37,8 @@ logger = logging.getLogger(__name__)
# 2.14: Add entry/exit orders to trade response
# 2.15: Add backtest history endpoints
# 2.16: Additional daily metrics
API_VERSION = 2.16
# 2.17: Forceentry - leverage, partial force_exit
API_VERSION = 2.17
# Public API, requires no auth.
router_public = APIRouter()
@@ -142,12 +143,11 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading'])
def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount else None
entry_tag = payload.entry_tag if payload.entry_tag else 'force_entry'
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
order_type=ordertype, stake_amount=stake_amount,
enter_tag=entry_tag)
order_type=ordertype, stake_amount=payload.stakeamount,
enter_tag=payload.entry_tag or 'force_entry',
leverage=payload.leverage)
if trade:
return ForceEnterResponse.parse_obj(trade.to_json())
@@ -161,7 +161,7 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_force_exit(payload.tradeid, ordertype)
return rpc._rpc_force_exit(payload.tradeid, ordertype, amount=payload.amount)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])

View File

@@ -179,8 +179,10 @@ class RPC:
else:
current_rate = trade.close_rate
if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio(current_rate)
current_profit_abs = trade.calc_profit(current_rate)
current_profit = trade.calc_profit_ratio(
current_rate) if not isnan(current_rate) else NAN
current_profit_abs = trade.calc_profit(
current_rate) if not isnan(current_rate) else NAN
current_profit_fiat: Optional[float] = None
# Calculate fiat profit
if self._fiat_converter:
@@ -239,12 +241,15 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError):
current_rate = NAN
if len(trade.select_filled_orders(trade.entry_side)) > 0:
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
trade_profit = NAN
profit_str = f'{NAN:.2%}'
else:
trade_profit = 0.0
profit_str = f'{0.0:.2f}'
if trade.nr_of_successful_entries > 0:
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
else:
trade_profit = 0.0
profit_str = f'{0.0:.2f}'
direction_str = ('S' if trade.is_short else 'L') if nonspot else ''
if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount(
@@ -424,8 +429,6 @@ class RPC:
for trade in trades:
current_rate: float = 0.0
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
@@ -447,9 +450,13 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError):
current_rate = NAN
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
profit_abs = trade.calc_profit(
rate=trade.close_rate or current_rate) + trade.realized_profit
if isnan(current_rate):
profit_ratio = NAN
profit_abs = NAN
else:
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
profit_abs = trade.calc_profit(
rate=trade.close_rate or current_rate) + trade.realized_profit
profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio)
@@ -660,36 +667,48 @@ class RPC:
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
def __exec_force_exit(self, trade: Trade, ordertype: Optional[str],
amount: Optional[float] = None) -> None:
# Check if there is there is an open order
fully_canceled = False
if trade.open_order_id:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == trade.entry_side:
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_EXIT'])
if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"force_exit", self._freqtrade.strategy.order_types["exit"])
sub_amount: Optional[float] = None
if amount and amount < trade.amount:
# Partial exit ...
min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
trade.pair, current_rate, trade.stop_loss_pct)
remaining = (trade.amount - amount) * current_rate
if remaining < min_exit_stake:
raise RPCException(f'Remaining amount of {remaining} would be too small.')
sub_amount = amount
self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type,
sub_trade_amt=sub_amount)
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *,
amount: Optional[float] = None) -> Dict[str, str]:
"""
Handler for forceexit <id>.
Sells the given trade at current price
"""
def _exec_force_exit(trade: Trade) -> None:
# Check if there is there is an open order
fully_canceled = False
if trade.open_order_id:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == trade.entry_side:
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_EXIT'])
if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"force_exit", self._freqtrade.strategy.order_types["exit"])
self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
@@ -698,7 +717,7 @@ class RPC:
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.get_open_trades():
_exec_force_exit(trade)
self.__exec_force_exit(trade, ordertype)
Trade.commit()
self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'}
@@ -711,7 +730,7 @@ class RPC:
logger.warning('force_exit: Invalid argument received')
raise RPCException('invalid argument')
_exec_force_exit(trade)
self.__exec_force_exit(trade, ordertype, amount)
Trade.commit()
self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'}
@@ -720,7 +739,8 @@ class RPC:
order_type: Optional[str] = None,
order_side: SignalDirection = SignalDirection.LONG,
stake_amount: Optional[float] = None,
enter_tag: Optional[str] = 'force_entry') -> Optional[Trade]:
enter_tag: Optional[str] = 'force_entry',
leverage: Optional[float] = None) -> Optional[Trade]:
"""
Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price
@@ -762,6 +782,7 @@ class RPC:
ordertype=order_type, trade=trade,
is_short=is_short,
enter_tag=enter_tag,
leverage_=leverage,
):
Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()

View File

@@ -0,0 +1,3 @@
# flake8: noqa: F401
from freqtrade.util.ft_precise import FtPrecise
from freqtrade.util.periodic_cache import PeriodicCache

View File

@@ -0,0 +1,12 @@
"""
Slim wrapper around ccxt's Precise (string math)
To have imports from freqtrade - and support float initializers
"""
from ccxt import Precise
class FtPrecise(Precise):
def __init__(self, number, decimals=None):
if not isinstance(number, str):
number = str(number)
super().__init__(number, decimals)