Merge branch 'develop' into feat/short
This commit is contained in:
@@ -115,15 +115,18 @@ class Ftx(Exchange):
|
||||
if order[0].get('status') == 'closed':
|
||||
# Trigger order was triggered ...
|
||||
real_order_id = order[0].get('info', {}).get('orderId')
|
||||
# OrderId may be None for stoploss-market orders
|
||||
# But contains "average" in these cases.
|
||||
if real_order_id:
|
||||
order1 = self._api.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order1)
|
||||
# Fake type to stop - as this was really a stop order.
|
||||
order1['id_stop'] = order1['id']
|
||||
order1['id'] = order_id
|
||||
order1['type'] = 'stop'
|
||||
order1['status_stop'] = 'triggered'
|
||||
return order1
|
||||
|
||||
order1 = self._api.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order1)
|
||||
# Fake type to stop - as this was really a stop order.
|
||||
order1['id_stop'] = order1['id']
|
||||
order1['id'] = order_id
|
||||
order1['type'] = 'stop'
|
||||
order1['status_stop'] = 'triggered'
|
||||
return order1
|
||||
return order[0]
|
||||
else:
|
||||
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
||||
|
@@ -344,29 +344,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
|
||||
def handle_insufficient_funds(self, trade: Trade):
|
||||
"""
|
||||
Determine if we ever opened a exiting order for this trade.
|
||||
If not, try update entering fees - otherwise "refind" the open order we obviously lost.
|
||||
"""
|
||||
exit_order = trade.select_order(trade.exit_side, None)
|
||||
if exit_order:
|
||||
self.refind_lost_order(trade)
|
||||
else:
|
||||
self.reupdate_enter_order_fees(trade)
|
||||
|
||||
def reupdate_enter_order_fees(self, trade: Trade):
|
||||
"""
|
||||
Get buy order from database, and try to reupdate.
|
||||
Handles trades where the initial fee-update did not work.
|
||||
"""
|
||||
logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}")
|
||||
order = trade.select_order(trade.enter_side, False)
|
||||
if order:
|
||||
logger.info(
|
||||
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
|
||||
def refind_lost_order(self, trade):
|
||||
"""
|
||||
Try refinding a lost trade.
|
||||
Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
|
||||
@@ -379,9 +356,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
if not order.ft_is_open:
|
||||
logger.debug(f"Order {order} is no longer open.")
|
||||
continue
|
||||
if order.ft_order_side == trade.enter_side:
|
||||
# Skip buy side - this is handled by reupdate_enter_order_fees
|
||||
continue
|
||||
try:
|
||||
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
|
||||
order.ft_order_side == 'stoploss')
|
||||
@@ -393,6 +367,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open order
|
||||
trade.open_order_id = order.order_id
|
||||
elif order.ft_order_side == trade.enter_side:
|
||||
if fo and fo['status'] == 'open':
|
||||
trade.open_order_id = order.order_id
|
||||
if fo:
|
||||
logger.info(f"Found {order} for trade {trade}.")
|
||||
self.update_trade_state(trade, order.order_id, fo,
|
||||
@@ -1241,12 +1218,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
filled_val = order.get('filled', 0.0) or 0.0
|
||||
filled_val: float = order.get('filled', 0.0) or 0.0
|
||||
filled_stake = filled_val * trade.open_rate
|
||||
minstake = self.exchange.get_min_pair_stake_amount(
|
||||
trade.pair, trade.open_rate, self.strategy.stoploss)
|
||||
|
||||
if filled_val > 0 and filled_stake < minstake:
|
||||
if filled_val > 0 and minstake and filled_stake < minstake:
|
||||
logger.warning(
|
||||
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
|
||||
f"as the filled amount of {filled_val} would result in an unexitable trade.")
|
||||
|
@@ -29,18 +29,23 @@ def decimals_per_coin(coin: str):
|
||||
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
|
||||
|
||||
|
||||
def round_coin_value(value: float, coin: str, show_coin_name=True) -> str:
|
||||
def round_coin_value(
|
||||
value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str:
|
||||
"""
|
||||
Get price value for this coin
|
||||
:param value: Value to be printed
|
||||
:param coin: Which coin are we printing the price / value for
|
||||
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
|
||||
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
||||
:return: Formatted / rounded value (with or without coin name)
|
||||
"""
|
||||
val = f"{value:.{decimals_per_coin(coin)}f}"
|
||||
if not keep_trailing_zeros:
|
||||
val = val.rstrip('0').rstrip('.')
|
||||
if show_coin_name:
|
||||
return f"{value:.{decimals_per_coin(coin)}f} {coin}"
|
||||
else:
|
||||
return f"{value:.{decimals_per_coin(coin)}f}"
|
||||
val = f"{val} {coin}"
|
||||
|
||||
return val
|
||||
|
||||
|
||||
def shorten_date(_date: str) -> str:
|
||||
|
@@ -408,6 +408,18 @@ class Backtesting:
|
||||
# use Open rate if open_rate > calculated sell rate
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
if (
|
||||
trade_dur == 0
|
||||
# Red candle (for longs), TODO: green candle (for shorts)
|
||||
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle
|
||||
and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate
|
||||
and close_rate > sell_row[CLOSE_IDX]
|
||||
):
|
||||
# ROI on opening candles with custom pricing can only
|
||||
# trigger if the entry was at Open or lower.
|
||||
# details: https: // github.com/freqtrade/freqtrade/issues/6261
|
||||
# If open_rate is < open, only allow sells below the close on red candles.
|
||||
raise ValueError("Opening candle ROI on red candles.")
|
||||
# Use the maximum between close_rate and low as we
|
||||
# cannot sell outside of a candle.
|
||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
||||
@@ -471,7 +483,10 @@ class Backtesting:
|
||||
trade.close_date = sell_candle_time
|
||||
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
try:
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
except ValueError:
|
||||
return None
|
||||
# call the custom exit price,with default value as previous closerate
|
||||
current_profit = trade.calc_profit_ratio(closerate)
|
||||
order_type = self.strategy.order_types['sell']
|
||||
|
@@ -373,7 +373,7 @@ class HyperoptTools():
|
||||
|
||||
trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
|
||||
lambda x: "{} {}".format(
|
||||
round_coin_value(x['max_drawdown_abs'], stake_currency),
|
||||
round_coin_value(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True),
|
||||
(f"({x['max_drawdown_account']:,.2%})"
|
||||
if has_account_drawdown
|
||||
else f"({x['max_drawdown']:,.2%})"
|
||||
@@ -388,7 +388,7 @@ class HyperoptTools():
|
||||
|
||||
trials['Profit'] = trials.apply(
|
||||
lambda x: '{} {}'.format(
|
||||
round_coin_value(x['Total profit'], stake_currency),
|
||||
round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True),
|
||||
f"({x['Profit']:,.2%})".rjust(10, ' ')
|
||||
).rjust(25+len(stake_currency))
|
||||
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),
|
||||
|
@@ -208,6 +208,13 @@ def migrate_orders_table(engine, table_back_name: str, cols: List):
|
||||
"""))
|
||||
|
||||
|
||||
def set_sqlite_to_wal(engine):
|
||||
if engine.name == 'sqlite' and str(engine.url) != 'sqlite://':
|
||||
# Set Mode to
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text("PRAGMA journal_mode=wal"))
|
||||
|
||||
|
||||
def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
"""
|
||||
Checks if migration is necessary and migrates if necessary
|
||||
@@ -235,3 +242,4 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
if 'orders' not in previous_tables and 'trades' in previous_tables:
|
||||
logger.info('Moving open orders to Orders table.')
|
||||
migrate_open_orders_to_trades(engine)
|
||||
set_sqlite_to_wal(engine)
|
||||
|
@@ -40,6 +40,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
"""
|
||||
kwargs = {}
|
||||
|
||||
if db_url == 'sqlite:///':
|
||||
raise OperationalException(
|
||||
f'Bad db-url {db_url}. For in-memory database, please use `sqlite://`.')
|
||||
if db_url == 'sqlite://':
|
||||
kwargs.update({
|
||||
'poolclass': StaticPool,
|
||||
@@ -885,13 +888,13 @@ class LocalTrade():
|
||||
self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]:
|
||||
"""
|
||||
Finds latest order for this orderside and status
|
||||
:param order_side: Side of the order (either 'buy' or 'sell')
|
||||
:param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
|
||||
:param is_open: Only search for open orders?
|
||||
:return: latest Order object if it exists, else None
|
||||
"""
|
||||
orders = self.orders
|
||||
if order_side:
|
||||
orders = [o for o in self.orders if o.side == order_side]
|
||||
orders = [o for o in self.orders if o.ft_order_side == order_side]
|
||||
if is_open is not None:
|
||||
orders = [o for o in orders if o.ft_is_open == is_open]
|
||||
if len(orders) > 0:
|
||||
@@ -1025,11 +1028,11 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
fee_close = Column(Float, nullable=False, default=0.0)
|
||||
fee_close_cost = Column(Float, nullable=True)
|
||||
fee_close_currency = Column(String(25), nullable=True)
|
||||
open_rate = Column(Float)
|
||||
open_rate: float = Column(Float)
|
||||
open_rate_requested = Column(Float)
|
||||
# open_trade_value - calculated via _calc_open_trade_value
|
||||
open_trade_value = Column(Float)
|
||||
close_rate = Column(Float)
|
||||
close_rate: Optional[float] = Column(Float)
|
||||
close_rate_requested = Column(Float)
|
||||
close_profit = Column(Float)
|
||||
close_profit_abs = Column(Float)
|
||||
|
@@ -20,6 +20,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||
# flake8: noqa: C901
|
||||
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
|
||||
config=Depends(get_config)):
|
||||
"""Start backtesting if not done so already"""
|
||||
@@ -32,6 +33,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||
for setting in settings.keys():
|
||||
if settings[setting] is not None:
|
||||
btconfig[setting] = settings[setting]
|
||||
try:
|
||||
btconfig['stake_amount'] = float(btconfig['stake_amount'])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Force dry-run for backtesting
|
||||
btconfig['dry_run'] = True
|
||||
@@ -57,8 +62,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||
):
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
ApiServer._bt = Backtesting(btconfig)
|
||||
if ApiServer._bt.timeframe_detail:
|
||||
ApiServer._bt.load_bt_data_detail()
|
||||
ApiServer._bt.load_bt_data_detail()
|
||||
else:
|
||||
ApiServer._bt.config = btconfig
|
||||
ApiServer._bt.init_backtest()
|
||||
|
@@ -152,7 +152,7 @@ class ShowConfig(BaseModel):
|
||||
trading_mode: str
|
||||
short_allowed: bool
|
||||
stake_currency: str
|
||||
stake_amount: Union[float, str]
|
||||
stake_amount: str
|
||||
available_capital: Optional[float]
|
||||
stake_currency_decimals: int
|
||||
max_open_trades: int
|
||||
@@ -291,6 +291,7 @@ class ForceEnterPayload(BaseModel):
|
||||
price: Optional[float]
|
||||
ordertype: Optional[OrderTypeValues]
|
||||
stakeamount: Optional[float]
|
||||
entry_tag: Optional[str]
|
||||
|
||||
|
||||
class ForceExitPayload(BaseModel):
|
||||
@@ -380,7 +381,7 @@ class BacktestRequest(BaseModel):
|
||||
timeframe_detail: Optional[str]
|
||||
timerange: Optional[str]
|
||||
max_open_trades: Optional[int]
|
||||
stake_amount: Optional[Union[float, str]]
|
||||
stake_amount: Optional[str]
|
||||
enable_protections: bool
|
||||
dry_run_wallet: Optional[float]
|
||||
|
||||
|
@@ -141,9 +141,11 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
||||
def forceentry(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 None
|
||||
|
||||
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
|
||||
order_type=ordertype, stake_amount=stake_amount)
|
||||
order_type=ordertype, stake_amount=stake_amount,
|
||||
enter_tag=entry_tag)
|
||||
|
||||
if trade:
|
||||
return ForceEnterResponse.parse_obj(trade.to_json())
|
||||
|
@@ -116,7 +116,7 @@ class RPC:
|
||||
'short_allowed': config.get('trading_mode', 'spot') != 'spot',
|
||||
'stake_currency': config['stake_currency'],
|
||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||
'stake_amount': config['stake_amount'],
|
||||
'stake_amount': str(config['stake_amount']),
|
||||
'available_capital': config.get('available_capital'),
|
||||
'max_open_trades': (config['max_open_trades']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
@@ -606,11 +606,6 @@ class RPC:
|
||||
'est_stake': est_stake or 0,
|
||||
'stake': stake_currency,
|
||||
})
|
||||
if total == 0.0:
|
||||
if self._freqtrade.config['dry_run']:
|
||||
raise RPCException('Running in Dry Run, balances are not available.')
|
||||
else:
|
||||
raise RPCException('All balances are zero.')
|
||||
|
||||
value = self._fiat_converter.convert_amount(
|
||||
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||
@@ -727,7 +722,8 @@ class RPC:
|
||||
def _rpc_force_entry(self, pair: str, price: Optional[float], *,
|
||||
order_type: Optional[str] = None,
|
||||
order_side: SignalDirection = SignalDirection.LONG,
|
||||
stake_amount: Optional[float] = None) -> Optional[Trade]:
|
||||
stake_amount: Optional[float] = None,
|
||||
enter_tag: Optional[str] = None) -> Optional[Trade]:
|
||||
"""
|
||||
Handler for forcebuy <asset> <price>
|
||||
Buys a pair trade at the given or current price
|
||||
@@ -765,7 +761,8 @@ class RPC:
|
||||
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
||||
ordertype=order_type, trade=trade,
|
||||
is_short=(order_side == SignalDirection.SHORT)
|
||||
is_short=(order_side == SignalDirection.SHORT),
|
||||
enter_tag=enter_tag,
|
||||
):
|
||||
Trade.commit()
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
|
@@ -813,12 +813,13 @@ class Telegram(RPCHandler):
|
||||
output = ''
|
||||
if self._config['dry_run']:
|
||||
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||
|
||||
output += ("Starting capital: "
|
||||
f"`{result['starting_capital']}` {self._config['stake_currency']}"
|
||||
)
|
||||
output += (f" `{result['starting_capital_fiat']}` "
|
||||
f"{self._config['fiat_display_currency']}.\n"
|
||||
starting_cap = round_coin_value(
|
||||
result['starting_capital'], self._config['stake_currency'])
|
||||
output += f"Starting capital: `{starting_cap}`"
|
||||
starting_cap_fiat = round_coin_value(
|
||||
result['starting_capital_fiat'], self._config['fiat_display_currency']
|
||||
) if result['starting_capital_fiat'] > 0 else ''
|
||||
output += (f" `, {starting_cap_fiat}`.\n"
|
||||
) if result['starting_capital_fiat'] > 0 else '.\n'
|
||||
|
||||
total_dust_balance = 0
|
||||
@@ -937,10 +938,11 @@ class Telegram(RPCHandler):
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||
try:
|
||||
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
if pair != 'cancel':
|
||||
try:
|
||||
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _forceenter_inline(self, update: Update, _: CallbackContext) -> None:
|
||||
if update.callback_query:
|
||||
@@ -975,12 +977,13 @@ class Telegram(RPCHandler):
|
||||
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
||||
pair_buttons = [
|
||||
InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}")
|
||||
for pair in whitelist
|
||||
for pair in sorted(whitelist)
|
||||
]
|
||||
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
|
||||
|
||||
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
|
||||
self._send_msg(msg="Which pair?",
|
||||
keyboard=self._layout_inline_keyboard(pair_buttons),
|
||||
callback_path="update_forcelong",
|
||||
keyboard=buttons_aligned,
|
||||
query=update.callback_query)
|
||||
|
||||
@authorized_only
|
||||
|
@@ -800,7 +800,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
return False
|
||||
|
||||
def should_exit(self, trade: Trade, rate: float, date: datetime, *,
|
||||
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
|
||||
enter: bool, exit_: bool,
|
||||
low: float = None, high: float = None,
|
||||
force_stoploss: float = 0) -> SellCheckTuple:
|
||||
@@ -819,7 +819,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
trade.adjust_min_max_rates(high or current_rate, low or current_rate)
|
||||
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
current_time=current_time,
|
||||
current_profit=current_profit,
|
||||
force_stoploss=force_stoploss, low=low, high=high)
|
||||
|
||||
# Set current rate to high for backtesting sell
|
||||
@@ -829,7 +830,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
|
||||
roi_reached = (not (enter and self.ignore_roi_if_buy_signal)
|
||||
and self.min_roi_reached(trade=trade, current_profit=current_profit,
|
||||
current_time=date))
|
||||
current_time=current_time))
|
||||
|
||||
sell_signal = SellType.NONE
|
||||
custom_reason = ''
|
||||
@@ -846,8 +847,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
trade_type = "exit_short" if trade.is_short else "sell"
|
||||
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
|
||||
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate,
|
||||
current_profit=current_profit)
|
||||
pair=trade.pair, trade=trade, current_time=current_time,
|
||||
current_rate=current_rate, current_profit=current_profit)
|
||||
if custom_reason:
|
||||
sell_signal = SellType.CUSTOM_SELL
|
||||
if isinstance(custom_reason, str):
|
||||
|
Reference in New Issue
Block a user