Merge pull request #9750 from freqtrade/feat/order_tags
Add tags per order
This commit is contained in:
@@ -767,6 +767,7 @@ This callback is **not** called when there is an open order (either buy or sell)
|
|||||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||||
|
|
||||||
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
||||||
|
Adjustment orders can be assigned with a tag by returning a 2 element Tuple, with the first element being the adjustment amount, and the 2nd element the tag (e.g. `return 250, 'increase_favorable_conditions'`).
|
||||||
|
|
||||||
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
||||||
|
|
||||||
@@ -833,7 +834,8 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
min_stake: Optional[float], max_stake: float,
|
min_stake: Optional[float], max_stake: float,
|
||||||
current_entry_rate: float, current_exit_rate: float,
|
current_entry_rate: float, current_exit_rate: float,
|
||||||
current_entry_profit: float, current_exit_profit: float,
|
current_entry_profit: float, current_exit_profit: float,
|
||||||
**kwargs) -> Optional[float]:
|
**kwargs
|
||||||
|
) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]:
|
||||||
"""
|
"""
|
||||||
Custom trade adjustment logic, returning the stake amount that a trade should be
|
Custom trade adjustment logic, returning the stake amount that a trade should be
|
||||||
increased or decreased.
|
increased or decreased.
|
||||||
@@ -859,11 +861,12 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
:return float: Stake amount to adjust your trade,
|
:return float: Stake amount to adjust your trade,
|
||||||
Positive values to increase position, Negative values to decrease position.
|
Positive values to increase position, Negative values to decrease position.
|
||||||
Return None for no action.
|
Return None for no action.
|
||||||
|
Optionally, return a tuple with a 2nd element with an order reason
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
|
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
|
||||||
# Take half of the profit at +5%
|
# Take half of the profit at +5%
|
||||||
return -(trade.stake_amount / 2)
|
return -(trade.stake_amount / 2), 'half_profit_5%'
|
||||||
|
|
||||||
if current_profit > -0.05:
|
if current_profit > -0.05:
|
||||||
return None
|
return None
|
||||||
@@ -891,7 +894,7 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
stake_amount = filled_entries[0].stake_amount
|
stake_amount = filled_entries[0].stake_amount
|
||||||
# This then calculates current safety order size
|
# This then calculates current safety order size
|
||||||
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
||||||
return stake_amount
|
return stake_amount, '1/3rd_increase'
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -645,8 +645,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
|
max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
|
||||||
stake_available = self.wallets.get_available_stake_amount()
|
stake_available = self.wallets.get_available_stake_amount()
|
||||||
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
||||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
|
||||||
default_retval=None, supress_error=True)(
|
|
||||||
trade=trade,
|
trade=trade,
|
||||||
current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
|
current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
|
||||||
current_profit=current_entry_profit, min_stake=min_entry_stake,
|
current_profit=current_entry_profit, min_stake=min_entry_stake,
|
||||||
@@ -665,7 +664,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
logger.debug("Max adjustment entries is set to unlimited.")
|
logger.debug("Max adjustment entries is set to unlimited.")
|
||||||
self.execute_entry(trade.pair, stake_amount, price=current_entry_rate,
|
self.execute_entry(trade.pair, stake_amount, price=current_entry_rate,
|
||||||
trade=trade, is_short=trade.is_short, mode='pos_adjust')
|
trade=trade, is_short=trade.is_short, mode='pos_adjust',
|
||||||
|
enter_tag=order_tag)
|
||||||
|
|
||||||
if stake_amount is not None and stake_amount < 0.0:
|
if stake_amount is not None and stake_amount < 0.0:
|
||||||
# We should decrease our position
|
# We should decrease our position
|
||||||
@@ -684,7 +684,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
|
self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
|
||||||
exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount)
|
exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount, exit_tag=order_tag)
|
||||||
|
|
||||||
def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
|
def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -782,6 +782,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
leverage=leverage
|
leverage=leverage
|
||||||
)
|
)
|
||||||
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
|
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
|
||||||
|
order_obj.ft_order_tag = enter_tag
|
||||||
order_id = order['id']
|
order_id = order['id']
|
||||||
order_status = order.get('status')
|
order_status = order.get('status')
|
||||||
logger.info(f"Order {order_id} was created for {pair} and status is {order_status}.")
|
logger.info(f"Order {order_id} was created for {pair} and status is {order_status}.")
|
||||||
@@ -1753,6 +1754,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
|
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
|
||||||
|
order_obj.ft_order_tag = exit_reason
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
|
|
||||||
trade.exit_order_status = ''
|
trade.exit_order_status = ''
|
||||||
|
|||||||
@@ -537,14 +537,14 @@ class Backtesting:
|
|||||||
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
||||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||||
stake_available = self.wallets.get_available_stake_amount()
|
stake_available = self.wallets.get_available_stake_amount()
|
||||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
|
||||||
default_retval=None, supress_error=True)(
|
|
||||||
trade=trade, # type: ignore[arg-type]
|
trade=trade, # type: ignore[arg-type]
|
||||||
current_time=current_time, current_rate=current_rate,
|
current_time=current_time, current_rate=current_rate,
|
||||||
current_profit=current_profit, min_stake=min_stake,
|
current_profit=current_profit, min_stake=min_stake,
|
||||||
max_stake=min(max_stake, stake_available),
|
max_stake=min(max_stake, stake_available),
|
||||||
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
||||||
current_entry_profit=current_profit, current_exit_profit=current_profit)
|
current_entry_profit=current_profit, current_exit_profit=current_profit
|
||||||
|
)
|
||||||
|
|
||||||
# Check if we should increase our position
|
# Check if we should increase our position
|
||||||
if stake_amount is not None and stake_amount > 0.0:
|
if stake_amount is not None and stake_amount > 0.0:
|
||||||
@@ -554,7 +554,8 @@ class Backtesting:
|
|||||||
check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment)
|
check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment)
|
||||||
if check_adjust_entry:
|
if check_adjust_entry:
|
||||||
pos_trade = self._enter_trade(
|
pos_trade = self._enter_trade(
|
||||||
trade.pair, row, 'short' if trade.is_short else 'long', stake_amount, trade)
|
trade.pair, row, 'short' if trade.is_short else 'long', stake_amount, trade,
|
||||||
|
entry_tag1=order_tag)
|
||||||
if pos_trade is not None:
|
if pos_trade is not None:
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
return pos_trade
|
return pos_trade
|
||||||
@@ -569,7 +570,7 @@ class Backtesting:
|
|||||||
if min_stake and remaining != 0 and remaining < min_stake:
|
if min_stake and remaining != 0 and remaining < min_stake:
|
||||||
# Remaining stake is too low to be sold.
|
# Remaining stake is too low to be sold.
|
||||||
return trade
|
return trade
|
||||||
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT, order_tag)
|
||||||
pos_trade = self._get_exit_for_signal(trade, row, exit_, current_time, amount)
|
pos_trade = self._get_exit_for_signal(trade, row, exit_, current_time, amount)
|
||||||
if pos_trade is not None:
|
if pos_trade is not None:
|
||||||
order = pos_trade.orders[-1]
|
order = pos_trade.orders[-1]
|
||||||
@@ -681,11 +682,11 @@ class Backtesting:
|
|||||||
|
|
||||||
trade.exit_reason = exit_reason
|
trade.exit_reason = exit_reason
|
||||||
|
|
||||||
return self._exit_trade(trade, row, close_rate, amount_)
|
return self._exit_trade(trade, row, close_rate, amount_, exit_reason)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
|
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, close_rate: float,
|
||||||
close_rate: float, amount: Optional[float] = None) -> Optional[LocalTrade]:
|
amount: float, exit_reason: Optional[str]) -> Optional[LocalTrade]:
|
||||||
self.order_id_counter += 1
|
self.order_id_counter += 1
|
||||||
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||||
order_type = self.strategy.order_types['exit']
|
order_type = self.strategy.order_types['exit']
|
||||||
@@ -712,6 +713,7 @@ class Backtesting:
|
|||||||
filled=0,
|
filled=0,
|
||||||
remaining=amount,
|
remaining=amount,
|
||||||
cost=amount * close_rate,
|
cost=amount * close_rate,
|
||||||
|
ft_order_tag=exit_reason,
|
||||||
)
|
)
|
||||||
order._trade_bt = trade
|
order._trade_bt = trade
|
||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
@@ -835,7 +837,9 @@ class Backtesting:
|
|||||||
stake_amount: Optional[float] = None,
|
stake_amount: Optional[float] = None,
|
||||||
trade: Optional[LocalTrade] = None,
|
trade: Optional[LocalTrade] = None,
|
||||||
requested_rate: Optional[float] = None,
|
requested_rate: Optional[float] = None,
|
||||||
requested_stake: Optional[float] = None) -> Optional[LocalTrade]:
|
requested_stake: Optional[float] = None,
|
||||||
|
entry_tag1: Optional[str] = None
|
||||||
|
) -> Optional[LocalTrade]:
|
||||||
"""
|
"""
|
||||||
:param trade: Trade to adjust - initial entry if None
|
:param trade: Trade to adjust - initial entry if None
|
||||||
:param requested_rate: Adjusted entry rate
|
:param requested_rate: Adjusted entry rate
|
||||||
@@ -843,7 +847,7 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
current_time = row[DATE_IDX].to_pydatetime()
|
current_time = row[DATE_IDX].to_pydatetime()
|
||||||
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
|
entry_tag = entry_tag1 or (row[ENTER_TAG_IDX] if len(row) >= ENTER_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
|
||||||
order_type = self.strategy.order_types['entry']
|
order_type = self.strategy.order_types['entry']
|
||||||
pos_adjust = trade is not None and requested_rate is None
|
pos_adjust = trade is not None and requested_rate is None
|
||||||
@@ -944,6 +948,7 @@ class Backtesting:
|
|||||||
filled=0,
|
filled=0,
|
||||||
remaining=amount,
|
remaining=amount,
|
||||||
cost=amount * propose_rate + trade.fee_open,
|
cost=amount * propose_rate + trade.fee_open,
|
||||||
|
ft_order_tag=entry_tag,
|
||||||
)
|
)
|
||||||
order._trade_bt = trade
|
order._trade_bt = trade
|
||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
@@ -963,7 +968,8 @@ class Backtesting:
|
|||||||
# Ignore trade if entry-order did not fill yet
|
# Ignore trade if entry-order did not fill yet
|
||||||
continue
|
continue
|
||||||
exit_row = data[pair][-1]
|
exit_row = data[pair][-1]
|
||||||
self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount)
|
self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount,
|
||||||
|
ExitType.FORCE_EXIT.value)
|
||||||
trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade)
|
trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade)
|
||||||
|
|
||||||
trade.close_date = exit_row[DATE_IDX].to_pydatetime()
|
trade.close_date = exit_row[DATE_IDX].to_pydatetime()
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
|||||||
ft_amount = get_column_def(cols_order, 'ft_amount', 'coalesce(amount, 0.0)')
|
ft_amount = get_column_def(cols_order, 'ft_amount', 'coalesce(amount, 0.0)')
|
||||||
ft_price = get_column_def(cols_order, 'ft_price', 'coalesce(price, 0.0)')
|
ft_price = get_column_def(cols_order, 'ft_price', 'coalesce(price, 0.0)')
|
||||||
ft_cancel_reason = get_column_def(cols_order, 'ft_cancel_reason', 'null')
|
ft_cancel_reason = get_column_def(cols_order, 'ft_cancel_reason', 'null')
|
||||||
|
ft_order_tag = get_column_def(cols_order, 'ft_order_tag', 'null')
|
||||||
|
|
||||||
# sqlite does not support literals for booleans
|
# sqlite does not support literals for booleans
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
@@ -230,13 +231,14 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
|||||||
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||||
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
|
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
|
||||||
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee,
|
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee,
|
||||||
ft_amount, ft_price, ft_cancel_reason
|
ft_amount, ft_price, ft_cancel_reason, ft_order_tag
|
||||||
)
|
)
|
||||||
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||||
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
|
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
|
||||||
cost, {stop_price} stop_price, order_date, order_filled_date,
|
cost, {stop_price} stop_price, order_date, order_filled_date,
|
||||||
order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee,
|
order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee,
|
||||||
{ft_amount} ft_amount, {ft_price} ft_price, {ft_cancel_reason} ft_cancel_reason
|
{ft_amount} ft_amount, {ft_price} ft_price, {ft_cancel_reason} ft_cancel_reason,
|
||||||
|
{ft_order_tag} ft_order_tag
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
@@ -331,8 +333,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||||||
# if ('orders' not in previous_tables
|
# if ('orders' not in previous_tables
|
||||||
# or not has_column(cols_orders, 'funding_fee')):
|
# or not has_column(cols_orders, 'funding_fee')):
|
||||||
migrating = False
|
migrating = False
|
||||||
# if not has_column(cols_orders, 'ft_cancel_reason'):
|
# if not has_column(cols_trades, 'funding_fee_running'):
|
||||||
if not has_column(cols_trades, 'funding_fee_running'):
|
if not has_column(cols_orders, 'ft_order_tag'):
|
||||||
migrating = True
|
migrating = True
|
||||||
logger.info(f"Running database migration for trades - "
|
logger.info(f"Running database migration for trades - "
|
||||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ class Order(ModelBase):
|
|||||||
funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
|
funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
|
||||||
|
|
||||||
ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
|
ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
|
||||||
|
ft_order_tag: Mapped[Optional[str]] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def order_date_utc(self) -> datetime:
|
def order_date_utc(self) -> datetime:
|
||||||
@@ -212,6 +214,10 @@ class Order(ModelBase):
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
|
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
:param minified: If True, only return a subset of the data is returned.
|
||||||
|
Only used for backtesting.
|
||||||
|
"""
|
||||||
resp = {
|
resp = {
|
||||||
'amount': self.safe_amount,
|
'amount': self.safe_amount,
|
||||||
'safe_price': self.safe_price,
|
'safe_price': self.safe_price,
|
||||||
@@ -219,6 +225,7 @@ class Order(ModelBase):
|
|||||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||||
'ft_is_entry': self.ft_order_side == entry_side,
|
'ft_is_entry': self.ft_order_side == entry_side,
|
||||||
|
'ft_order_tag': self.ft_order_tag,
|
||||||
}
|
}
|
||||||
if not minified:
|
if not minified:
|
||||||
resp.update({
|
resp.update({
|
||||||
@@ -1405,6 +1412,7 @@ class LocalTrade:
|
|||||||
ft_price=order["price"],
|
ft_price=order["price"],
|
||||||
remaining=order["remaining"],
|
remaining=order["remaining"],
|
||||||
funding_fee=order.get("funding_fee", None),
|
funding_fee=order.get("funding_fee", None),
|
||||||
|
ft_order_tag=order.get("ft_order_tag", None),
|
||||||
)
|
)
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
|
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ class OrderSchema(BaseModel):
|
|||||||
order_timestamp: Optional[int] = None
|
order_timestamp: Optional[int] = None
|
||||||
order_filled_timestamp: Optional[int] = None
|
order_filled_timestamp: Optional[int] = None
|
||||||
ft_fee_base: Optional[float] = None
|
ft_fee_base: Optional[float] = None
|
||||||
|
ft_order_tag: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class TradeSchema(BaseModel):
|
class TradeSchema(BaseModel):
|
||||||
|
|||||||
@@ -511,7 +511,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
min_stake: Optional[float], max_stake: float,
|
min_stake: Optional[float], max_stake: float,
|
||||||
current_entry_rate: float, current_exit_rate: float,
|
current_entry_rate: float, current_exit_rate: float,
|
||||||
current_entry_profit: float, current_exit_profit: float,
|
current_entry_profit: float, current_exit_profit: float,
|
||||||
**kwargs) -> Optional[float]:
|
**kwargs
|
||||||
|
) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]:
|
||||||
"""
|
"""
|
||||||
Custom trade adjustment logic, returning the stake amount that a trade should be
|
Custom trade adjustment logic, returning the stake amount that a trade should be
|
||||||
increased or decreased.
|
increased or decreased.
|
||||||
@@ -537,6 +538,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
:return float: Stake amount to adjust your trade,
|
:return float: Stake amount to adjust your trade,
|
||||||
Positive values to increase position, Negative values to decrease position.
|
Positive values to increase position, Negative values to decrease position.
|
||||||
Return None for no action.
|
Return None for no action.
|
||||||
|
Optionally, return a tuple with a 2nd element with an order reason
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -725,6 +727,36 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
_ft_stop_uses_after_fill = False
|
_ft_stop_uses_after_fill = False
|
||||||
|
|
||||||
|
def _adjust_trade_position_internal(
|
||||||
|
self, trade: Trade, current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float,
|
||||||
|
min_stake: Optional[float], max_stake: float,
|
||||||
|
current_entry_rate: float, current_exit_rate: float,
|
||||||
|
current_entry_profit: float, current_exit_profit: float,
|
||||||
|
**kwargs
|
||||||
|
) -> Tuple[Optional[float], str]:
|
||||||
|
"""
|
||||||
|
wrapper around adjust_trade_position to handle the return value
|
||||||
|
"""
|
||||||
|
resp = strategy_safe_wrapper(self.adjust_trade_position,
|
||||||
|
default_retval=(None, ''), supress_error=True)(
|
||||||
|
trade=trade, current_time=current_time,
|
||||||
|
current_rate=current_rate, current_profit=current_profit,
|
||||||
|
min_stake=min_stake, max_stake=max_stake,
|
||||||
|
current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
|
||||||
|
current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
order_tag = ''
|
||||||
|
if isinstance(resp, tuple):
|
||||||
|
if len(resp) >= 1:
|
||||||
|
stake_amount = resp[0]
|
||||||
|
if len(resp) > 1:
|
||||||
|
order_tag = resp[1] or ''
|
||||||
|
else:
|
||||||
|
stake_amount = resp
|
||||||
|
return stake_amount, order_tag
|
||||||
|
|
||||||
def __informative_pairs_freqai(self) -> ListPairsWithTimeframes:
|
def __informative_pairs_freqai(self) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
Create informative-pairs needed for FreqAI
|
Create informative-pairs needed for FreqAI
|
||||||
|
|||||||
@@ -742,14 +742,18 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
'orders': [
|
'orders': [
|
||||||
[
|
[
|
||||||
{'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy',
|
{'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy',
|
||||||
'order_filled_timestamp': 1517251200000, 'ft_is_entry': True},
|
'order_filled_timestamp': 1517251200000, 'ft_is_entry': True,
|
||||||
|
'ft_order_tag': ''},
|
||||||
{'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell',
|
{'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell',
|
||||||
'order_filled_timestamp': 1517265300000, 'ft_is_entry': False}
|
'order_filled_timestamp': 1517265300000, 'ft_is_entry': False,
|
||||||
|
'ft_order_tag': 'roi'}
|
||||||
], [
|
], [
|
||||||
{'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy',
|
{'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy',
|
||||||
'order_filled_timestamp': 1517283000000, 'ft_is_entry': True},
|
'order_filled_timestamp': 1517283000000, 'ft_is_entry': True,
|
||||||
|
'ft_order_tag': ''},
|
||||||
{'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell',
|
{'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell',
|
||||||
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False}
|
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False,
|
||||||
|
'ft_order_tag': 'roi'}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||||||
assert pytest.approx(trade.amount) == 47.61904762 * leverage
|
assert pytest.approx(trade.amount) == 47.61904762 * leverage
|
||||||
assert len(trade.orders) == 1
|
assert len(trade.orders) == 1
|
||||||
# Increase position by 100
|
# Increase position by 100
|
||||||
backtesting.strategy.adjust_trade_position = MagicMock(return_value=100)
|
backtesting.strategy.adjust_trade_position = MagicMock(return_value=(100, 'PartIncrease'))
|
||||||
|
|
||||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||||
|
|
||||||
@@ -156,6 +156,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||||||
assert pytest.approx(trade.stake_amount) == 200.0
|
assert pytest.approx(trade.stake_amount) == 200.0
|
||||||
assert pytest.approx(trade.amount) == 95.23809524 * leverage
|
assert pytest.approx(trade.amount) == 95.23809524 * leverage
|
||||||
assert len(trade.orders) == 2
|
assert len(trade.orders) == 2
|
||||||
|
assert trade.orders[-1].ft_order_tag == 'PartIncrease'
|
||||||
assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791)
|
assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791)
|
||||||
|
|
||||||
# Reduce by more than amount - no change to trade.
|
# Reduce by more than amount - no change to trade.
|
||||||
@@ -171,13 +172,14 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
|
|||||||
assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791)
|
assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791)
|
||||||
|
|
||||||
# Reduce position by 50
|
# Reduce position by 50
|
||||||
backtesting.strategy.adjust_trade_position = MagicMock(return_value=-100)
|
backtesting.strategy.adjust_trade_position = MagicMock(return_value=(-100, 'partDecrease'))
|
||||||
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||||
|
|
||||||
assert trade
|
assert trade
|
||||||
assert pytest.approx(trade.stake_amount) == 100.0
|
assert pytest.approx(trade.stake_amount) == 100.0
|
||||||
assert pytest.approx(trade.amount) == 47.61904762 * leverage
|
assert pytest.approx(trade.amount) == 47.61904762 * leverage
|
||||||
assert len(trade.orders) == 3
|
assert len(trade.orders) == 3
|
||||||
|
assert trade.orders[-1].ft_order_tag == 'partDecrease'
|
||||||
assert trade.nr_of_successful_entries == 2
|
assert trade.nr_of_successful_entries == 2
|
||||||
assert trade.nr_of_successful_exits == 1
|
assert trade.nr_of_successful_exits == 1
|
||||||
assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791)
|
assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||||
'remaining': ANY, 'status': ANY, 'ft_is_entry': True, 'ft_fee_base': None,
|
'remaining': ANY, 'status': ANY, 'ft_is_entry': True, 'ft_fee_base': None,
|
||||||
'funding_fee': ANY,
|
'funding_fee': ANY, 'ft_order_tag': None,
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|||||||
@@ -6725,11 +6725,15 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca
|
|||||||
)
|
)
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=10)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(10, 'aaaa'))
|
||||||
freqtrade.process_open_trade_positions()
|
freqtrade.process_open_trade_positions()
|
||||||
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)
|
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)
|
||||||
|
assert freqtrade.strategy.adjust_trade_position.call_count == 1
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-10)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-10, 'partial_exit_c'))
|
||||||
freqtrade.process_open_trade_positions()
|
freqtrade.process_open_trade_positions()
|
||||||
assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog)
|
assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog)
|
||||||
|
assert freqtrade.strategy.adjust_trade_position.call_count == 1
|
||||||
|
trade = Trade.get_trades(trade_filter=[Trade.id == 5]).first()
|
||||||
|
assert trade.orders[-1].ft_order_tag == 'partial_exit_c'
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ def test_dca_order_adjust_entry_replace_fails(
|
|||||||
# Create DCA order for 2nd trade (so we have 2 open orders on 2 trades)
|
# Create DCA order for 2nd trade (so we have 2 open orders on 2 trades)
|
||||||
# this 2nd order won't fill.
|
# this 2nd order won't fill.
|
||||||
|
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=20)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(20, 'PeNF'))
|
||||||
|
|
||||||
freqtrade.process()
|
freqtrade.process()
|
||||||
|
|
||||||
@@ -627,12 +627,13 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
|
|||||||
assert log_has_re(
|
assert log_has_re(
|
||||||
r"Remaining amount of \d\.\d+.* would be smaller than the minimum of 10.", caplog)
|
r"Remaining amount of \d\.\d+.* would be smaller than the minimum of 10.", caplog)
|
||||||
|
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-20, 'PES'))
|
||||||
|
|
||||||
freqtrade.process()
|
freqtrade.process()
|
||||||
trade = Trade.get_trades().first()
|
trade = Trade.get_trades().first()
|
||||||
assert len(trade.orders) == 2
|
assert len(trade.orders) == 2
|
||||||
assert trade.orders[-1].ft_order_side == 'sell'
|
assert trade.orders[-1].ft_order_side == 'sell'
|
||||||
|
assert trade.orders[-1].ft_order_tag == 'PES'
|
||||||
assert pytest.approx(trade.stake_amount) == 40.198
|
assert pytest.approx(trade.stake_amount) == 40.198
|
||||||
assert pytest.approx(trade.amount) == 20.099 * leverage
|
assert pytest.approx(trade.amount) == 20.099 * leverage
|
||||||
assert trade.open_rate == 2.0
|
assert trade.open_rate == 2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user