Skip to content
All library documents

TWAP Order Slicing with RSI-Based Entry and Exit Signals

Article Strategy library · Author: freqtrade

Summary

This Freqtrade example demonstrates splitting trade entries and exits into equal time-based portions. It sets a per-slice stake from the proposed amount, then uses filled order counts and timestamps to decide when another entry or exit portion is due. The example sets ten slices with one minute between them and uses RSI thresholds with positive volume as sample entry conditions.

Exit slicing begins when the RSI condition is met, and subsequent portions are spaced by the same interval. The document provides implementation details but no performance results. Its entry and exit signals are explicitly illustrative, and the code does not establish that slicing will reduce market impact or improve execution. The approach also depends on strategy callbacks and order-fill behavior, so it should be assessed in the target trading environment before use.

Key ideas

  • The strategy divides entries and exits into a configured number of portions.
  • It schedules later entry portions from the last filled order time.
  • RSI thresholds provide example signals for entering long or short positions.
  • An RSI condition triggers partial exits, which are then spaced over time.
  • The document offers no evidence that the execution method improves realized prices.

Tags

Full text
# TWAPStrategy


# TWAPStrategy









TWAP (Time-Weighted Average Price) execution on both entry and exit.
    this strategy breaks orders into equal time-based slices to minimize market impact
    The entry and exit signals are examples and should be adapted to your strategy.
    
    - twap_num_slices: desired number of execution slices.
    - twap_interval_minutes: time between execution slices.

## Source (GPL-3.0)

```python
from datetime import datetime, timedelta

import pandas as pd

from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy
import talib.abstract as ta


class TWAPStrategy(IStrategy):
    """
    TWAP (Time-Weighted Average Price) execution on both entry and exit.
    this strategy breaks orders into equal time-based slices to minimize market impact
    The entry and exit signals are examples and should be adapted to your strategy.
    
    - twap_num_slices: desired number of execution slices.
    - twap_interval_minutes: time between execution slices.
    """

    timeframe = "15m"
    stoploss = -0.10
    minimal_roi = {"0": 0.02}
    process_only_new_candles = True
    startup_candle_count = 30
    can_short = True
    position_adjustment_enable = True

    twap_num_slices = 10             # number of slices
    twap_interval_minutes = 1      # interval between slices


    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe["rsi"] = ta.RSI(dataframe)
        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:

        dataframe.loc[
            (dataframe["rsi"] < 45) & (dataframe["volume"] > 0),
            "enter_long",
        ] = 1

        # Short entry
        dataframe.loc[
            (dataframe["rsi"] > 55) & (dataframe["volume"] > 0),
            "enter_short",
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:

        return dataframe





    def should_exit_partially(self, trade: Trade, current_time: datetime) -> bool:
        """
        Determine whether the trade should be partially exited with slices.
        This method is only intended to be called from strategy callbacks.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(
            trade.pair, self.timeframe
        )

        if dataframe.empty:
            return False

        last_candle = dataframe.iloc[-1]
        rsi = last_candle["rsi"]

        if trade.is_short:
            return rsi < 45

        return rsi > 55

    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                             proposed_stake: float, min_stake: float | None, max_stake: float,
                             leverage: float, entry_tag: str | None, side: str,
                             **kwargs) -> float:

        return proposed_stake / self.twap_num_slices


    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                               current_rate: float, current_profit: float,
                               min_stake: float | None, max_stake: float,
                               current_entry_rate: float, current_exit_rate: float,
                               current_entry_profit: float, current_exit_profit: float,
                               **kwargs
                               ) -> float | None | tuple[float | None, str | None]:


        if trade.has_open_orders:
            return None

        filled_entries = trade.select_filled_orders(trade.entry_side)
        entry_slices_done = len(filled_entries)
        filled_exits = trade.select_filled_orders(trade.exit_side)
        exit_slices_done = len(filled_exits)

        already_exiting = exit_slices_done > 0

        if already_exiting or self.should_exit_partially(trade, current_time):
            return self._next_exit_slice(trade, current_time, filled_exits, exit_slices_done)

        if entry_slices_done < self.twap_num_slices:
            return self._next_entry_slice(trade, current_time, filled_entries, entry_slices_done)

        return None

    def _next_entry_slice(self, trade: Trade, current_time: datetime,
                           filled_entries: list, slices_done: int
                           ) -> float | None | tuple[float | None, str | None]:


        last_fill_time = filled_entries[-1].order_filled_utc if filled_entries else trade.open_date_utc
        next_slice_due_at = last_fill_time + timedelta(minutes=self.twap_interval_minutes)
        if current_time < next_slice_due_at:
            return None


        stake_already_filled = sum(o.stake_amount_filled for o in filled_entries)
        twap_total_stake = filled_entries[0].stake_amount_filled * self.twap_num_slices
        remaining_stake = twap_total_stake - stake_already_filled
        remaining_slices = self.twap_num_slices - slices_done


        next_slice_stake = remaining_stake if remaining_slices <= 1 else remaining_stake / remaining_slices

        if next_slice_stake < 0:
            return None

        return next_slice_stake

    def _next_exit_slice(self, trade: Trade, current_time: datetime,
                          filled_exits: list, slices_done: int
                          ) -> float | None | tuple[float | None, str | None]:

        if slices_done >= self.twap_num_slices:
            return None

        last_fill_time = filled_exits[-1].order_filled_utc if filled_exits else current_time
        next_slice_due_at = last_fill_time + timedelta(minutes=self.twap_interval_minutes)
        if slices_done > 0 and current_time < next_slice_due_at:
            return None

        remaining_slices = self.twap_num_slices - slices_done


        if remaining_slices <= 1:

            return -trade.stake_amount

        slice_stake = trade.stake_amount / remaining_slices
        return -slice_stake

```

Shown in full with attribution under the source's licence. Licence: GPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.