merged with feat/short

This commit is contained in:
Sam Germain
2021-09-27 23:26:20 -06:00
37 changed files with 528 additions and 300 deletions

View File

@@ -31,6 +31,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'profit_ratio', 'profit_abs', 'sell_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag']
# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?)
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:

View File

@@ -168,8 +168,8 @@ class Edge:
pair_data = pair_data.sort_values(by=['date'])
pair_data = pair_data.reset_index(drop=True)
df_analyzed = self.strategy.advise_sell(
dataframe=self.strategy.advise_buy(
df_analyzed = self.strategy.advise_exit(
dataframe=self.strategy.advise_entry(
dataframe=pair_data,
metadata={'pair': pair}
),

View File

@@ -15,8 +15,7 @@ class SignalTagType(Enum):
"""
Enum for signal columns
"""
BUY_TAG = "buy_tag"
SHORT_TAG = "short_tag"
ENTER_TAG = "enter_tag"
class SignalDirection(Enum):

View File

@@ -182,12 +182,6 @@ class Kraken(Exchange):
Kraken set's the leverage as an option in the order object, so we need to
add it to params
"""
if leverage > 1.0:
self._params['leverage'] = leverage
else:
if 'leverage' in self._params:
del self._params['leverage']
return
def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict:

View File

@@ -441,11 +441,11 @@ class FreqtradeBot(LoggingMixin):
return False
# running get_signal on historical data fetched
(side, enter_tag) = self.strategy.get_entry_signal(
(signal, enter_tag) = self.strategy.get_entry_signal(
pair, self.strategy.timeframe, analyzed_df
)
if side:
if signal:
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
@@ -585,7 +585,9 @@ class FreqtradeBot(LoggingMixin):
default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=max_stake_amount)
min_stake=min_stake_amount, max_stake=max_stake_amount, side='long')
# TODO-lev: Add non-hardcoded "side" parameter
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount:
@@ -603,10 +605,13 @@ class FreqtradeBot(LoggingMixin):
order_type = self.strategy.order_types.get('forcebuy', order_type)
# TODO-lev: Will this work for shorting?
# TODO-lev: Add non-hardcoded "side" parameter
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of {name.lower()}ing {pair}")
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
side='short' if is_short else 'long'
):
logger.info(f"User requested abortion of buying {pair}")
return False
amount = self.exchange.amount_to_precision(pair, amount)
order = self.exchange.create_order(

View File

@@ -45,8 +45,7 @@ LONG_IDX = 5
ELONG_IDX = 6 # Exit long
SHORT_IDX = 7
ESHORT_IDX = 8 # Exit short
BUY_TAG_IDX = 9
SHORT_TAG_IDX = 10
ENTER_TAG_IDX = 9
class Backtesting:
@@ -139,6 +138,10 @@ class Backtesting:
self.config['startup_candle_count'] = self.required_startup
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
# TODO-lev: This should come from the configuration setting or better a
# TODO-lev: combination of config/strategy "use_shorts"(?) and "can_short" from the exchange
self._can_short = False
self.progress = BTProgress()
self.abort = False
@@ -249,7 +252,7 @@ class Backtesting:
# Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'long_tag', 'short_tag']
'enter_short', 'exit_short', 'enter_tag']
data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed))
@@ -260,18 +263,10 @@ class Backtesting:
if not pair_data.empty:
# Cleanup from prior runs
# TODO-lev: The below is not 100% compatible with the interface compatibility layer
if 'enter_long' in pair_data.columns:
pair_data.loc[:, 'enter_long'] = 0
pair_data.loc[:, 'enter_short'] = 0
if 'exit_long' in pair_data.columns:
pair_data.loc[:, 'exit_long'] = 0
pair_data.loc[:, 'exit_short'] = 0
pair_data.loc[:, 'long_tag'] = None
pair_data.loc[:, 'short_tag'] = None
pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore')
df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}),
df_analyzed = self.strategy.advise_exit(
self.strategy.advise_entry(pair_data, {'pair': pair}),
{'pair': pair}
).copy()
# Trim startup period from analyzed dataframe
@@ -279,11 +274,11 @@ class Backtesting:
startup_candles=self.required_startup)
# To avoid using data from future, we use buy/sell signals shifted
# from the previous candle
df_analyzed.loc[:, 'enter_long'] = df_analyzed.loc[:, 'enter_long'].shift(1)
df_analyzed.loc[:, 'enter_short'] = df_analyzed.loc[:, 'enter_short'].shift(1)
df_analyzed.loc[:, 'exit_long'] = df_analyzed.loc[:, 'exit_long'].shift(1)
df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1)
df_analyzed.loc[:, 'long_tag'] = df_analyzed.loc[:, 'long_tag'].shift(1)
for col in headers[5:]:
if col in df_analyzed.columns:
df_analyzed.loc[:, col] = df_analyzed.loc[:, col].shift(1)
else:
df_analyzed.loc[:, col] = 0 if col != 'enter_tag' else None
# Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
@@ -434,7 +429,8 @@ class Backtesting:
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
side=direction)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount:
@@ -445,12 +441,13 @@ class Backtesting:
# Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX],
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime(),
side=direction):
return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
# Enter trade
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
has_enter_tag = len(row) >= ENTER_TAG_IDX + 1
trade = LocalTrade(
pair=pair,
open_rate=row[OPEN_IDX],
@@ -460,7 +457,7 @@ class Backtesting:
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
buy_tag=row[ENTER_TAG_IDX] if has_enter_tag else None,
exchange=self._exchange_name,
is_short=(direction == 'short'),
)
@@ -499,8 +496,8 @@ class Backtesting:
def check_for_trade_entry(self, row) -> Optional[str]:
enter_long = row[LONG_IDX] == 1
exit_long = row[ELONG_IDX] == 1
enter_short = row[SHORT_IDX] == 1
exit_short = row[ESHORT_IDX] == 1
enter_short = self._can_short and row[SHORT_IDX] == 1
exit_short = self._can_short and row[ESHORT_IDX] == 1
if enter_long == 1 and not any([exit_long, enter_short]):
# Long

View File

@@ -230,9 +230,9 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
pass
# TODO-lev: add side
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool:
time_in_force: str, current_time: datetime,
side: str, **kwargs) -> bool:
"""
Called right before placing a entry order.
Timing for this function is critical, so avoid doing heavy computations or
@@ -248,6 +248,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@@ -365,13 +366,11 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return None
# TODO-lev: add side
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
side: str, **kwargs) -> float:
"""
Customize stake size for each new trade. This method is not called when edge module is
enabled.
Customize stake size for each new trade.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
@@ -379,10 +378,28 @@ class IStrategy(ABC, HyperStrategyMixin):
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade. This method is not called when edge module is
enabled.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""
return 1.0
def informative_pairs(self) -> ListPairsWithTimeframes:
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
@@ -473,8 +490,8 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
logger.debug("TA Analysis Launched")
dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.advise_sell(dataframe, metadata)
dataframe = self.advise_entry(dataframe, metadata)
dataframe = self.advise_exit(dataframe, metadata)
return dataframe
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -503,8 +520,7 @@ class IStrategy(ABC, HyperStrategyMixin):
dataframe[SignalType.EXIT_LONG.value] = 0
dataframe[SignalType.ENTER_SHORT.value] = 0
dataframe[SignalType.EXIT_SHORT.value] = 0
dataframe[SignalTagType.BUY_TAG.value] = None
dataframe[SignalTagType.SHORT_TAG.value] = None
dataframe[SignalTagType.ENTER_TAG.value] = None
# Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
@@ -563,9 +579,8 @@ class IStrategy(ABC, HyperStrategyMixin):
message = ""
if dataframe is None:
message = "No dataframe returned (return statement missing?)."
elif 'buy' not in dataframe:
# TODO-lev: Something?
message = "Buy column not set."
elif 'enter_long' not in dataframe:
message = "enter_long/buy column not set."
elif df_len != len(dataframe):
message = message_template.format("length")
elif df_close != dataframe["close"].iloc[-1]:
@@ -675,10 +690,10 @@ class IStrategy(ABC, HyperStrategyMixin):
enter_tag_value: Optional[str] = None
if enter_long == 1 and not any([exit_long, enter_short]):
enter_signal = SignalDirection.LONG
enter_tag_value = latest.get(SignalTagType.BUY_TAG.value, None)
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
if enter_short == 1 and not any([exit_short, enter_long]):
enter_signal = SignalDirection.SHORT
enter_tag_value = latest.get(SignalTagType.SHORT_TAG.value, None)
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
timeframe_seconds = timeframe_to_seconds(timeframe)
@@ -897,7 +912,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
"""
Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advise_buy or advise_sell!
Does not run advise_entry or advise_exit!
Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run.
Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show.
@@ -929,7 +944,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
return self.populate_indicators(dataframe, metadata)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry order signal for the given dataframe
This method should not be overridden.
@@ -944,15 +959,15 @@ class IStrategy(ABC, HyperStrategyMixin):
if self._buy_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore
df = self.populate_buy_trend(dataframe) # type: ignore
else:
df = self.populate_buy_trend(dataframe, metadata)
if 'enter_long' not in df.columns:
df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns')
if 'enter_long' not in df.columns:
df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns')
return df
return df
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit order signal for the given dataframe
This method should not be overridden.
@@ -966,26 +981,9 @@ class IStrategy(ABC, HyperStrategyMixin):
if self._sell_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore
df = self.populate_sell_trend(dataframe) # type: ignore
else:
df = self.populate_sell_trend(dataframe, metadata)
if 'exit_long' not in df.columns:
df = df.rename({'sell': 'exit_long'}, axis='columns')
return df
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade. This method is not called when edge module is
enabled.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""
return 1.0
if 'exit_long' not in df.columns:
df = df.rename({'sell': 'exit_long'}, axis='columns')
return df

View File

@@ -15,8 +15,9 @@ import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
# TODO-lev: Create a meaningfull short strategy (not just revresed signs).
# This class is a sample. Feel free to customize it.
class SampleStrategy(IStrategy):
class SampleShortStrategy(IStrategy):
"""
This is a sample strategy to inspire you.
More information in https://www.freqtrade.io/en/latest/strategy-customization/

View File

@@ -12,12 +12,11 @@ def bot_loop_start(self, **kwargs) -> None:
"""
pass
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
side: str, **kwargs) -> float:
"""
Customize stake size for each new trade. This method is not called when edge module is
enabled.
Customize stake size for each new trade.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
@@ -25,6 +24,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake
@@ -80,9 +80,10 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
time_in_force: str, current_time: datetime,
side: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Called right before placing a entry order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
@@ -90,12 +91,13 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process