Merge pull request #9750 from freqtrade/feat/order_tags

Add tags per order
This commit is contained in:
Matthias 2024-02-01 07:11:06 +01:00 committed by GitHub
commit f7472ca74c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 99 additions and 34 deletions

View File

@ -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.
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.
@ -833,7 +834,8 @@ class DigDeeperStrategy(IStrategy):
min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: 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
increased or decreased.
@ -859,11 +861,12 @@ class DigDeeperStrategy(IStrategy):
:return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position.
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:
# 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:
return None
@ -891,7 +894,7 @@ class DigDeeperStrategy(IStrategy):
stake_amount = filled_entries[0].stake_amount
# This then calculates current safety order size
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
return stake_amount
return stake_amount, '1/3rd_increase'
except Exception as exception:
return None

View File

@ -645,8 +645,7 @@ class FreqtradeBot(LoggingMixin):
max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
stake_available = self.wallets.get_available_stake_amount()
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None, supress_error=True)(
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
trade=trade,
current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
current_profit=current_entry_profit, min_stake=min_entry_stake,
@ -665,7 +664,8 @@ class FreqtradeBot(LoggingMixin):
else:
logger.debug("Max adjustment entries is set to unlimited.")
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:
# We should decrease our position
@ -684,7 +684,7 @@ class FreqtradeBot(LoggingMixin):
return
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:
"""
@ -782,6 +782,7 @@ class FreqtradeBot(LoggingMixin):
leverage=leverage
)
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_status = order.get('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
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.exit_order_status = ''

View File

@ -537,14 +537,14 @@ class Backtesting:
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)
stake_available = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None, supress_error=True)(
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
trade=trade, # type: ignore[arg-type]
current_time=current_time, current_rate=current_rate,
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,
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
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)
if check_adjust_entry:
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:
self.wallets.update()
return pos_trade
@ -569,7 +570,7 @@ class Backtesting:
if min_stake and remaining != 0 and remaining < min_stake:
# Remaining stake is too low to be sold.
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)
if pos_trade is not None:
order = pos_trade.orders[-1]
@ -681,11 +682,11 @@ class Backtesting:
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
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
close_rate: float, amount: Optional[float] = None) -> Optional[LocalTrade]:
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, close_rate: float,
amount: float, exit_reason: Optional[str]) -> Optional[LocalTrade]:
self.order_id_counter += 1
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
order_type = self.strategy.order_types['exit']
@ -712,6 +713,7 @@ class Backtesting:
filled=0,
remaining=amount,
cost=amount * close_rate,
ft_order_tag=exit_reason,
)
order._trade_bt = trade
trade.orders.append(order)
@ -835,7 +837,9 @@ class Backtesting:
stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = 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 requested_rate: Adjusted entry rate
@ -843,7 +847,7 @@ class Backtesting:
"""
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
order_type = self.strategy.order_types['entry']
pos_adjust = trade is not None and requested_rate is None
@ -944,6 +948,7 @@ class Backtesting:
filled=0,
remaining=amount,
cost=amount * propose_rate + trade.fee_open,
ft_order_tag=entry_tag,
)
order._trade_bt = trade
trade.orders.append(order)
@ -963,7 +968,8 @@ class Backtesting:
# Ignore trade if entry-order did not fill yet
continue
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.close_date = exit_row[DATE_IDX].to_pydatetime()

View File

@ -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_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_order_tag = get_column_def(cols_order, 'ft_order_tag', 'null')
# sqlite does not support literals for booleans
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,
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,
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,
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
cost, {stop_price} stop_price, order_date, order_filled_date,
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}
"""))
@ -331,8 +333,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# if ('orders' not in previous_tables
# or not has_column(cols_orders, 'funding_fee')):
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
logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}")

View File

@ -89,6 +89,8 @@ class Order(ModelBase):
funding_fee: 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
def order_date_utc(self) -> datetime:
@ -212,6 +214,10 @@ class Order(ModelBase):
return order
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 = {
'amount': self.safe_amount,
'safe_price': self.safe_price,
@ -219,6 +225,7 @@ class Order(ModelBase):
'order_filled_timestamp': int(self.order_filled_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
'ft_is_entry': self.ft_order_side == entry_side,
'ft_order_tag': self.ft_order_tag,
}
if not minified:
resp.update({
@ -1405,6 +1412,7 @@ class LocalTrade:
ft_price=order["price"],
remaining=order["remaining"],
funding_fee=order.get("funding_fee", None),
ft_order_tag=order.get("ft_order_tag", None),
)
trade.orders.append(order_obj)

View File

@ -261,6 +261,7 @@ class OrderSchema(BaseModel):
order_timestamp: Optional[int] = None
order_filled_timestamp: Optional[int] = None
ft_fee_base: Optional[float] = None
ft_order_tag: Optional[str] = None
class TradeSchema(BaseModel):

View File

@ -511,7 +511,8 @@ class IStrategy(ABC, HyperStrategyMixin):
min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: 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
increased or decreased.
@ -537,6 +538,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position.
Return None for no action.
Optionally, return a tuple with a 2nd element with an order reason
"""
return None
@ -725,6 +727,36 @@ class IStrategy(ABC, HyperStrategyMixin):
_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:
"""
Create informative-pairs needed for FreqAI

View File

@ -742,14 +742,18 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'orders': [
[
{'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',
'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',
'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',
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False}
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False,
'ft_order_tag': 'roi'}
]
]
})

View File

@ -148,7 +148,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
assert pytest.approx(trade.amount) == 47.61904762 * leverage
assert len(trade.orders) == 1
# 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)
@ -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.amount) == 95.23809524 * leverage
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)
# 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)
# 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)
assert trade
assert pytest.approx(trade.stake_amount) == 100.0
assert pytest.approx(trade.amount) == 47.61904762 * leverage
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_exits == 1
assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791)

View File

@ -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,
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
'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())

View File

@ -6725,11 +6725,15 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca
)
create_mock_trades(fee)
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()
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)
assert freqtrade.strategy.adjust_trade_position.call_count == 1
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()
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'

View File

@ -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)
# 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()
@ -627,12 +627,13 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
assert log_has_re(
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()
trade = Trade.get_trades().first()
assert len(trade.orders) == 2
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.amount) == 20.099 * leverage
assert trade.open_rate == 2.0