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
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
[…] Signal and Trade Candle […]