Signal and Trade Candle

Users tend to get confused over these two candles. What is signal candle? What is trade candle?

Signal candle is the candle that have the enter/exit signal. This candle is used to determine whether a trade should be opened/closed. This candle also consist all the latest indicators calculated just before an entry order is opened.

Trade candle is the candle where the entry/exit order filled in the exchange. This candle can be the candle just after the signal candle, or several candles after signal candle if you set a long timeout on limit entry order.

Take a look at the image below

signal and trade candles

Candle 1 is the signal candle. After candle 1 closed, the bot fetched the data, calculate indicators, and check entry and exit signals. Turned out the entry signal is triggered and exit signal isn’t. The bot then opened an entry order around the open of candle 2, and it got filled successfully. That makes Candle 2 become the trade candle.

Is it matter though?

Yes it is, if you need some indicators’ value to manage the open trade, or when you are evaluating closed trades. Take this real case for example. You want to set the limit entry order’s rate to be equal to signal candle’s close rate, and the bot did exactly that. Your trade’s open rate equal to candle 1’s close rate. But then you went through past data on Frequi, you checked the OHLCV values on candle 2, you compared the close rate of candle 2 to the trade’s open rate, and you can’t figure out why they don’t match.

Another example. you want to set your custom_exit to exit a trade when current_rate exceed (open rate + atr of signal candle). If you pick the wrong candle, the atr used will be different than what you think it should use, hence the exit might be different that what you intended.

How to get them

First of all, you need to know that there would be times where you can’t get the signal/trade candle. The bot only has startup_candle_count amount of candles stored, which means if your trade duration is too long and going past the startup_candle_count amount of candles, you won’t be able to retrieve the signal/trade candle.

To get the trade candle is quite easy

entry_time = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
trade_candle = dataframe.loc[dataframe['date'] == entry_time]
# Trade candle found
if not trade_candle.empty:
    # do something here

Getting signal candle is more tricky. If you are using market entry order or you are using limit entry order with timeout setting within the same entry candle, then signal candle is one candle before trade candle

signal_time = entry_time - timedelta(minutes=timeframe_to_minutes(self.timeframe))
signal_candle = dataframe.loc[dataframe['date'] == signal_time]
# Signal candle found
if not signal_candle.empty:
    #do something here

But if you set a long limit timeout, then above code might give you wrong candle, because the trade candle might have been few candles after signal candle. You need to get the time the entry order was opened. The snippet below is better than above because it would work for both cases.

filled_entries = trade.select_filled_orders(trade.entry_side)
first_order = filled_entries[0]
first_order_open_time = timeframe_to_prev_date(self.timeframe, first_order.order_date_utc)
signal_time = first_order_open_time - timedelta(minutes=timeframe_to_minutes(self.timeframe))
signal_candle = dataframe.loc[dataframe['date'] == signal_time]
# Signal candle found
if not signal_candle.empty:
    #do something here

Next thing to note, if you are gonna use the signal/trade candle inside custom_exit/stoploss functions, it not a good practice to keep searching and calling the candle on every bot loop (default is 5 seconds). You should store the data inside a dictionary.

custom_info_candle = dict()
init_custom_info_candle_dict = {
    'signal': None,
    'trade': None,
}
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
    
    dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
    if pair not in self.custom_info_candle:
        self.custom_info_candle[pair] = self.init_custom_info_candle_dict.copy()
    if (self.custom_info_candle[pair]['signal'] is None) and (self.custom_info_candle[pair]['not_found'] == False):
        entry_time = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
        trade_candle = dataframe.loc[dataframe['date'] == entry_time]
        # Trade candle found
        if not trade_candle.empty:
            self.custom_info_candle[pair]['trade'] = trade_candle.squeeze()
            filled_entries = trade.select_filled_orders(trade.entry_side)
            first_order = filled_entries[0]
            first_order_open_time = timeframe_to_prev_date(self.timeframe, first_order.order_date_utc)
            signal_time = first_order_open_time - timedelta(minutes=timeframe_to_minutes(self.timeframe))
            signal_candle = dataframe.loc[dataframe['date'] == signal_time]
            # Signal candle found
            if not signal_candle.empty:
                self.custom_info_candle[pair]['signal'] = signal_candle.squeeze()
            else:
                # Trade has been opened for too long. Signal candles can't be found
                self.custom_info_candle[pair]['not_found'] = True
        else:
            # Trade has been opened for too long. Trade and signal candles can't be found
            self.custom_info_candle[pair]['not_found'] = True

Let’s say you want to have new exit logic. The trade should closed when last closed candle’s close rate equal or above signal candle’s ema_9

if (self.custom_info_candle[pair]['signal'] is not None):
    if (current_candle['close'] >= self.custom_info_candle[pair]['signal']['ema_9']):
        return "close_above_ema_9_signal"

Don’t forget to reset the dictionary after a trade is closed.

def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool:
    val = super().confirm_trade_exit(pair, trade, order_type, amount, rate, time_in_force, exit_reason, current_time, **kwargs)
    if val:
        if (pair in self.custom_info_candle):
            self.custom_info_candle[pair] = self.init_custom_info_candle_dict.copy()
    return val

So the full code of example strategy should look like this

from freqtrade.strategy import IStrategy, informative
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
import talib.abstract as ta
from freqtrade.persistence import Trade
from datetime import datetime, timedelta
from typing import Optional, Union
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_minutes
def EWO(source, sma_length=5, sma2_length=35):
    sma1 = ta.SMA(source, timeperiod=sma_length)
    sma2 = ta.SMA(source, timeperiod=sma2_length)
    smadif = (sma1 - sma2) / source * 100
    return smadif
class strat_template (IStrategy):
    def version(self) -> str:
        return "template-v1"
    INTERFACE_VERSION = 3
    minimal_roi = {
        "0": 0.05
    }
    stoploss = -0.05
    timeframe = '15m'
    process_only_new_candles = True
    startup_candle_count = 999
    custom_info_candle = dict()
    init_custom_info_candle_dict = {
        'signal': None,
        'trade': None,
        'not_found': False,
    }
    use_custom_stoploss = True
    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float:
        
        sl_new = 1
        if (current_time - timedelta(minutes=15) >= trade.open_date_utc):
            enter_tags = trade.enter_tag.split()
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            current_candle = dataframe.iloc[-1].squeeze()
            current_profit = trade.calc_profit_ratio(current_candle['close'])
            if ('close_below_9' in enter_tags):
                # only close_below_9 tag
                if (len(enter_tags) == 1):
                    if (current_profit >= 0.01):
                        # use tighter tsl offset
                        sl_new = 0.0025
                else:
                    if (current_profit >= 0.02):
                        sl_new = 0.005
            elif (current_profit >= 0.03):
                sl_new = 0.01
        return sl_new
    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
        if ((current_time - timedelta(minutes=15)) >= trade.open_date_utc):
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if pair not in self.custom_info_candle:
                self.custom_info_candle[pair] = self.init_custom_info_candle_dict.copy()
            if (self.custom_info_candle[pair]['signal'] is None) and (self.custom_info_candle[pair]['not_found'] == False):
                entry_time = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
                trade_candle = dataframe.loc[dataframe['date'] == entry_time]
                # Trade candle found
                if not trade_candle.empty:
                    self.custom_info_candle[pair]['trade'] = trade_candle.squeeze()
                    filled_entries = trade.select_filled_orders(trade.entry_side)
                    first_order = filled_entries[0]
                    first_order_open_time = timeframe_to_prev_date(self.timeframe, first_order.order_date_utc)
                    signal_time = first_order_open_time - timedelta(minutes=timeframe_to_minutes(self.timeframe))
                    signal_candle = dataframe.loc[dataframe['date'] == signal_time]
                    # Signal candle found
                    if not signal_candle.empty:
                        self.custom_info_candle[pair]['signal'] = signal_candle.squeeze()
                    else:
                        # Trade has been opened for too long. Signal candles can't be found
                        self.custom_info_candle[pair]['not_found'] = True
                else:
                    # Trade has been opened for too long. Trade and signal candles can't be found
                    self.custom_info_candle[pair]['not_found'] = True
            current_candle = dataframe.iloc[-1].squeeze()
            current_profit = trade.calc_profit_ratio(current_candle['close'])
            
            if (current_profit >= 0):
                if (current_candle['rsi'] >= 70):
                    return "rsi_overbought"
            if (self.custom_info_candle[pair]['signal'] is not None):
                if (current_candle['close'] >= self.custom_info_candle[pair]['signal']['ema_9']):
                    return "close_above_ema_9_signal"
        return None
    def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool:
        val = super().confirm_trade_exit(pair, trade, order_type, amount, rate, time_in_force, exit_reason, current_time, **kwargs)
        if val:
            if (pair in self.custom_info_candle):
                self.custom_info_candle[pair] = self.init_custom_info_candle_dict.copy()
        return val
    @informative('30m')
    def populate_indicators_inf1(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        
        dataframe['rsi'] = ta.RSI(dataframe['close'], 14)
        return dataframe
    @informative('1h')
    def populate_indicators_inf2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        
        dataframe['rsi'] = ta.RSI(dataframe['close'], 14)
        return dataframe
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['ema_9'] = ta.EMA(dataframe['close'], 9)
        dataframe['ema_20'] = ta.EMA(dataframe['close'], 20)
        dataframe['rsi'] = ta.RSI(dataframe['close'], 14)
        dataframe['ema_9_rsi'] = ta.EMA(dataframe['rsi'], 9)
        dataframe['ewo'] = EWO(dataframe['close'], 50, 200)
        return dataframe
    
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['enter_tag'] = ''
        enter_1 = (
            qtpylib.crossed_above(dataframe['ema_9'], dataframe['ema_20'])
            &
            (dataframe['rsi_30m'] < 50)
            &
            (dataframe['rsi_1h'] < 30)
            &
            (dataframe['ema_9_rsi'] < 70)
            &
            (dataframe['ewo'] > 3)
            &
            (dataframe['volume'] > 0)
        )
        enter_2 = (
            (dataframe['close'] < dataframe['ema_9'])
            &
            (dataframe['volume'] > 0)
        )
        enter_3 = (
            (dataframe['close'] < dataframe['ema_20'])
            &
            (dataframe['volume'] > 0)
        )
        dataframe.loc[
            enter_1 | enter_2 | enter_3
            , 'enter_long'
        ] = 1
        dataframe.loc[
            enter_1
            , 'enter_tag'
        ] += 'golden_cross '
        dataframe.loc[
            enter_2
            , 'enter_tag'
        ] += 'close_below_9 '
        dataframe.loc[
            enter_3
            , 'enter_tag'
        ] += 'close_below_20 '
        return dataframe
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['exit_tag'] = ''
        exit_1 = (
            qtpylib.crossed_below(dataframe['ema_9'], dataframe['ema_20'])
            &
            (dataframe['volume'] > 0)
        )
        exit_2 = (
            (dataframe['close'] > dataframe['ema_9'])
            &
            (dataframe['volume'] > 0)
        )
        exit_3 = (
            (dataframe['close'] > dataframe['ema_20'])
            &
            (dataframe['volume'] > 0)
        )
        dataframe.loc[
            exit_1 | exit_2 | exit_3
            , 'exit_long'
        ] = 1
        dataframe.loc[
            exit_1
            , 'exit_tag'
        ] += 'death_cross '
        dataframe.loc[
            exit_2
            , 'exit_tag'
        ] += 'close_above_9 '
        dataframe.loc[
            exit_3
            , 'exit_tag'
        ] += 'close_above_20 '
        return dataframe

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *