updated tests
This commit is contained in:
@@ -13,6 +13,7 @@ class ExitType(Enum):
|
||||
FORCE_SELL = "force_sell"
|
||||
EMERGENCY_SELL = "emergency_sell"
|
||||
CUSTOM_SELL = "custom_sell"
|
||||
PARTIAL_SELL = "partial_sell"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -1428,8 +1428,9 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_rate(self, pair: str, refresh: bool,
|
||||
side: Literal['entry', 'exit'], is_short: bool, order_book: Optional[dict] = None, ticker: Optional[dict] = Non) -> float:
|
||||
def get_rate(self, pair: str, refresh: bool, # noqa: max-complexity: 13
|
||||
side: Literal['entry', 'exit'], is_short: bool,
|
||||
order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float:
|
||||
"""
|
||||
Calculates bid/ask target
|
||||
bid rate - between current ask price and last price
|
||||
@@ -1533,7 +1534,8 @@ class Exchange:
|
||||
if not entry_rate:
|
||||
entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
|
||||
if not exit_rate:
|
||||
exit_rate = self.get_rate(pair, refresh, 'exit', order_book=order_book, ticker=ticker)
|
||||
exit_rate = self.get_rate(pair, refresh, 'exit',
|
||||
is_short, order_book=order_book, ticker=ticker)
|
||||
return entry_rate, exit_rate
|
||||
|
||||
# Fee handling
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -459,7 +459,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
if signal:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
|
||||
|
||||
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
|
||||
if ((bid_check_dom.get('enabled', False)) and
|
||||
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
||||
@@ -505,7 +504,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
If the strategy triggers the adjustment, a new order gets issued.
|
||||
Once that completes, the existing trade is modified to match new data.
|
||||
"""
|
||||
current_entry_rate, current_exit_rate = self.exchange.get_rates(trade.pair, True, is_short)
|
||||
current_entry_rate, current_exit_rate = self.exchange.get_rates(
|
||||
trade.pair, True, trade.is_short)
|
||||
|
||||
current_entry_profit = trade.calc_profit_ratio(current_entry_rate)
|
||||
current_exit_profit = trade.calc_profit_ratio(current_exit_rate)
|
||||
@@ -528,7 +528,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
|
||||
current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit,
|
||||
min_entry_stake=min_entry_stake, min_exit_stake=min_exit_stake,
|
||||
max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_exit_stake, stake_available),
|
||||
max_entry_stake=min(max_entry_stake, stake_available),
|
||||
max_exit_stake=min(max_exit_stake, stake_available)
|
||||
)
|
||||
|
||||
if stake_amount is not None and stake_amount > 0.0:
|
||||
@@ -540,7 +541,8 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
return
|
||||
else:
|
||||
logger.debug("Max adjustment entries is set to unlimited.")
|
||||
self.execute_entry(trade.pair, stake_amount, current_entry_rate, trade=trade, is_short=trade.is_short)
|
||||
self.execute_entry(trade.pair, stake_amount, current_entry_rate,
|
||||
trade=trade, is_short=trade.is_short)
|
||||
|
||||
if stake_amount is not None and stake_amount < 0.0:
|
||||
# We should decrease our position
|
||||
@@ -553,8 +555,8 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
logger.info(
|
||||
f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
|
||||
amount = trade.amount
|
||||
self.execute_trade_exit(trade, current_exit_rate, sell_reason=SellCheckTuple(
|
||||
sell_type=SellType.CUSTOM_SELL), sub_trade_amt=amount)
|
||||
self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
|
||||
exit_type=ExitType.PARTIAL_SELL), sub_trade_amt=amount)
|
||||
|
||||
def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
|
||||
"""
|
||||
@@ -628,7 +630,6 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
|
||||
amount = (stake_amount / enter_limit_requested) * leverage
|
||||
order_type = ordertype or self.strategy.order_types['entry']
|
||||
|
||||
if not pos_adjust and not strategy_safe_wrapper(
|
||||
self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
@@ -648,7 +649,7 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, side)
|
||||
order_id = order['id']
|
||||
order_status = order.get('status', None)
|
||||
order_status = order.get('status')
|
||||
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
|
||||
|
||||
# we assume the order is executed at the price requested
|
||||
@@ -744,8 +745,8 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
else:
|
||||
logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
|
||||
|
||||
# Update fees if order is closed
|
||||
if order_status == 'closed':
|
||||
# Update fees if order is non-opened
|
||||
if order_status in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
self.update_trade_state(trade, order_id, order)
|
||||
|
||||
return True
|
||||
@@ -1384,7 +1385,7 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
# if stoploss is on exchange and we are on dry_run mode,
|
||||
# we consider the sell price stop price
|
||||
if (self.config['dry_run'] and exit_type == 'stoploss'
|
||||
and self.strategy.order_types['stoploss_on_exchange']):
|
||||
and self.strategy.order_types['stoploss_on_exchange']):
|
||||
limit = trade.stop_loss
|
||||
|
||||
# set custom_exit_price if available
|
||||
@@ -1471,7 +1472,7 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
profit_rate = order.safe_price
|
||||
|
||||
if not fill:
|
||||
trade.process_sell_sub_trade(order, is_closed=False)
|
||||
trade.process_exit_sub_trade(order, is_closed=False)
|
||||
|
||||
profit_ratio = trade.close_profit
|
||||
profit = trade.close_profit_abs
|
||||
@@ -1637,12 +1638,12 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
# Updating wallets when order is closed
|
||||
self.wallets.update()
|
||||
|
||||
sub_trade = not isclose(order_obj.safe_amount_after_fee,
|
||||
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
|
||||
if not trade.is_open:
|
||||
self.handle_protections(trade.pair)
|
||||
sub_trade = order_obj.safe_amount_after_fee != trade.amount
|
||||
if order.get('side', None) == 'sell':
|
||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
self._notify_exit(trade, '', True, sub_trade=sub_trade, order=order_obj)
|
||||
self.handle_protections(trade.pair)
|
||||
elif send_msg and not trade.open_order_id:
|
||||
# Enter fill
|
||||
self._notify_enter(trade, order_obj, fill=True, sub_trade=sub_trade)
|
||||
@@ -1794,4 +1795,4 @@ max_entry_stake=min(max_entry_stake, stake_available), max_exit_stake=min(max_ex
|
||||
# Bracket between min_custom_price_allowed and max_custom_price_allowed
|
||||
return max(
|
||||
min(valid_custom_price, max_custom_price_allowed),
|
||||
min_custom_price_allowed)
|
||||
min_custom_price_allowed)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -480,10 +480,11 @@ class Backtesting:
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake, max_stake=min(max_stake, stake_available),
|
||||
current_profit=current_profit, min_stake=min_stake,
|
||||
max_stake=min(max_stake, stake_available),
|
||||
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
||||
max_entry_stake=min(max_stake, stake_available),
|
||||
max_exit_stake=min(max_stake, stake_available))
|
||||
max_entry_stake=min(max_stake, stake_available),
|
||||
max_exit_stake=min(max_stake, stake_available))
|
||||
|
||||
# Check if we should increase our position
|
||||
if stake_amount is not None and stake_amount > 0.0:
|
||||
@@ -586,7 +587,7 @@ max_exit_stake=min(max_stake, stake_available))
|
||||
close_rate: float, amount: float = None) -> Optional[LocalTrade]:
|
||||
self.order_id_counter += 1
|
||||
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||
order_type = self.strategy.order_types['sell']
|
||||
order_type = self.strategy.order_types['exit']
|
||||
amount = amount or trade.amount
|
||||
order = Order(
|
||||
id=self.order_id_counter,
|
||||
@@ -905,7 +906,7 @@ max_exit_stake=min(max_stake, stake_available))
|
||||
return None
|
||||
return row
|
||||
|
||||
def backtest(self, processed: Dict,
|
||||
def backtest(self, processed: Dict, # noqa: max-complexity: 13
|
||||
start_date: datetime, end_date: datetime,
|
||||
max_open_trades: int = 0, position_stacking: bool = False,
|
||||
enable_protections: bool = False) -> Dict[str, Any]:
|
||||
@@ -1007,7 +1008,7 @@ max_exit_stake=min(max_stake, stake_available))
|
||||
sub_trade = order.safe_amount_after_fee != trade.amount
|
||||
if sub_trade:
|
||||
order.close_bt_order(current_time)
|
||||
trade.process_sell_sub_trade(order)
|
||||
trade.process_exit_sub_trade(order)
|
||||
trade.recalc_trade_from_orders()
|
||||
else:
|
||||
trade.close_date = current_time
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ This module contains the class to persist trades into SQLite
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from math import isclose
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
|
||||
@@ -13,7 +14,7 @@ from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, MATH_CLOSE_PREC
|
||||
from freqtrade.enums import ExitType, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.leverage import interest
|
||||
@@ -193,7 +194,7 @@ class Order(_DECL_BASE):
|
||||
self.order_filled_date = datetime.now(timezone.utc)
|
||||
self.order_update_date = datetime.now(timezone.utc)
|
||||
|
||||
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
||||
def to_json(self, enter_side: str) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.ft_pair,
|
||||
'order_id': self.order_id,
|
||||
@@ -215,7 +216,7 @@ class Order(_DECL_BASE):
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||
'order_type': self.order_type,
|
||||
'price': self.price,
|
||||
'ft_is_entry': self.ft_order_side == entry_side,
|
||||
'ft_is_entry': self.ft_order_side == enter_side,
|
||||
'remaining': self.remaining,
|
||||
}
|
||||
|
||||
@@ -359,7 +360,7 @@ class LocalTrade():
|
||||
if self.has_no_leverage:
|
||||
return 0.0
|
||||
elif not self.is_short:
|
||||
return (self.amount * self.open_rate) * ((self.leverage-1)/self.leverage)
|
||||
return (self.amount * self.open_rate) * ((self.leverage - 1) / self.leverage)
|
||||
else:
|
||||
return self.amount
|
||||
|
||||
@@ -613,6 +614,9 @@ class LocalTrade():
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
else:
|
||||
logger.warning(
|
||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||
self.recalc_trade_from_orders()
|
||||
elif order.ft_order_side == self.exit_side:
|
||||
if self.is_open:
|
||||
@@ -622,10 +626,16 @@ class LocalTrade():
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
if self.amount == order.safe_amount_after_fee:
|
||||
else:
|
||||
logger.warning(
|
||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||
if isclose(order.safe_amount_after_fee,
|
||||
self.amount, abs_tol=MATH_CLOSE_PREC):
|
||||
self.close(order.safe_price)
|
||||
else:
|
||||
self.process_sell_sub_trade(order)
|
||||
logger.info((self.amount, self.to_json(), order.to_json(
|
||||
self.enter_side), order.safe_amount_after_fee))
|
||||
self.process_exit_sub_trade(order)
|
||||
elif order.ft_order_side == 'stoploss':
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
@@ -637,25 +647,29 @@ class LocalTrade():
|
||||
raise ValueError(f'Unknown order type: {order.order_type}')
|
||||
Trade.commit()
|
||||
|
||||
def process_sell_sub_trade(self, order: Order, is_closed: bool = True) -> None:
|
||||
sell_amount = order.safe_amount_after_fee
|
||||
sell_rate = order.safe_price
|
||||
sell_stake_amount = sell_rate * sell_amount * (1 - self.fee_close)
|
||||
profit = self.calc_profit2(self.open_rate, sell_rate, sell_amount)
|
||||
def process_exit_sub_trade(self, order: Order, is_closed: bool = True) -> None:
|
||||
exit_amount = order.safe_amount_after_fee
|
||||
exit_rate = order.safe_price
|
||||
exit_stake_amount = exit_rate * exit_amount * (1 - self.fee_close)
|
||||
profit = self.calc_profit2(self.open_rate, exit_rate, exit_amount)
|
||||
if is_closed:
|
||||
self.amount -= sell_amount
|
||||
self.amount -= exit_amount
|
||||
self.stake_amount = self.open_rate * self.amount
|
||||
self.realized_profit += profit
|
||||
logger.info(
|
||||
'Processed exit sub trade for %s',
|
||||
self
|
||||
)
|
||||
|
||||
self.close_profit_abs = profit
|
||||
self.close_profit = sell_stake_amount / (sell_stake_amount - profit) - 1
|
||||
self.close_profit = exit_stake_amount / (exit_stake_amount - profit) - 1
|
||||
self.recalc_open_trade_value()
|
||||
|
||||
def calc_profit2(self, open_rate: float, close_rate: float,
|
||||
amount: float) -> float:
|
||||
return float(Decimal(amount) *
|
||||
(Decimal(1 - self.fee_close) * Decimal(close_rate) -
|
||||
Decimal(1 + self.fee_open) * Decimal(open_rate)))
|
||||
return float(Decimal(amount)
|
||||
* (Decimal(1 - self.fee_close) * Decimal(close_rate)
|
||||
- Decimal(1 + self.fee_open) * Decimal(open_rate)))
|
||||
|
||||
def close(self, rate: float, *, show_msg: bool = True) -> None:
|
||||
"""
|
||||
@@ -729,7 +743,7 @@ class LocalTrade():
|
||||
def recalc_open_trade_value(self) -> None:
|
||||
"""
|
||||
Recalculate open_trade_value.
|
||||
Must be called whenever open_rate, fee_open or is_short is changed.
|
||||
Must be called whenever open_rate, fee_open is changed.
|
||||
"""
|
||||
self.open_trade_value = self._calc_open_trade_value()
|
||||
|
||||
@@ -747,7 +761,7 @@ class LocalTrade():
|
||||
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
|
||||
sec_per_hour = Decimal(3600)
|
||||
total_seconds = Decimal((now - open_date).total_seconds())
|
||||
hours = total_seconds/sec_per_hour or zero
|
||||
hours = total_seconds / sec_per_hour or zero
|
||||
|
||||
rate = Decimal(interest_rate or self.interest_rate)
|
||||
borrowed = Decimal(self.borrowed)
|
||||
@@ -861,9 +875,9 @@ class LocalTrade():
|
||||
return 0.0
|
||||
else:
|
||||
if self.is_short:
|
||||
profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage
|
||||
profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage
|
||||
else:
|
||||
profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage
|
||||
profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage
|
||||
|
||||
return float(f"{profit_ratio:.8f}")
|
||||
|
||||
@@ -878,11 +892,11 @@ class LocalTrade():
|
||||
|
||||
tmp_amount = o.safe_amount_after_fee
|
||||
tmp_price = o.safe_price
|
||||
is_sell = o.ft_order_side != 'buy'
|
||||
side = -1 if is_sell else 1
|
||||
is_exit = o.ft_order_side != self.enter_side
|
||||
side = -1 if is_exit else 1
|
||||
if tmp_amount > 0.0 and tmp_price is not None:
|
||||
total_amount += tmp_amount * side
|
||||
price = avg_price if is_sell else tmp_price
|
||||
price = avg_price if is_exit else tmp_price
|
||||
total_stake += price * tmp_amount * side
|
||||
if total_amount > 0:
|
||||
avg_price = total_stake / total_amount
|
||||
@@ -932,9 +946,9 @@ class LocalTrade():
|
||||
:return: array of Order objects
|
||||
"""
|
||||
return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
|
||||
and o.ft_is_open is False and
|
||||
(o.filled or 0) > 0 and
|
||||
o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
and o.ft_is_open is False
|
||||
and o.filled
|
||||
and o.status in NON_OPEN_EXCHANGE_STATES]
|
||||
|
||||
@property
|
||||
def nr_of_successful_entries(self) -> int:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -969,7 +969,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# Make sure current_profit is calculated using high for backtesting.
|
||||
bound = low if trade.is_short else high
|
||||
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
|
||||
|
||||
from pprint import pformat
|
||||
logger.info(pformat(trade.to_json()))
|
||||
logger.info((self.trailing_only_offset_is_reached,
|
||||
self.trailing_stop_positive, bound_profit, sl_offset))
|
||||
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
||||
f"offset: {sl_offset:.4g} profit: {current_profit:.2%}")
|
||||
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
||||
if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset):
|
||||
# Specific handling for trailing_stop_positive
|
||||
|
||||
Reference in New Issue
Block a user