EthFocusedInformativeDry.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. from __future__ import annotations
  2. from datetime import datetime
  3. import pandas as pd
  4. from freqtrade.persistence import Trade
  5. from freqtrade.strategy import IStrategy
  6. class EthFocusedInformativeDry(IStrategy):
  7. INTERFACE_VERSION = 3
  8. timeframe = "5m"
  9. can_short = False
  10. startup_candle_count = 480
  11. process_only_new_candles = True
  12. minimal_roi = {"0": 100.0}
  13. stoploss = -0.02
  14. use_exit_signal = True
  15. exit_profit_only = False
  16. ignore_roi_if_entry_signal = False
  17. eth_rsi_trend_sma = 120
  18. eth_rsi_length = 2
  19. eth_rsi_threshold = 3.0
  20. eth_exit_rsi = 55.0
  21. btc_trend_sma = 480
  22. btc_momentum_lookback = 240
  23. btc_min_momentum = 0.0
  24. lead_lookback_15m = 8
  25. lead_lookback_5m = 16
  26. btc_return_threshold_15m = 0.018
  27. btc_return_threshold_5m = 0.012
  28. lag_gap = 0.006
  29. lead_lag_max_hold_bars = 8
  30. lead_lag_stop_loss = -0.006
  31. lead_lag_take_profit = 0.018
  32. rsi_filter_leverage = 3.0
  33. lead_lag_leverage = 3.0
  34. def informative_pairs(self) -> list[tuple[str, str]]:
  35. return [
  36. ("BTC/USDT:USDT", "5m"),
  37. ("BTC/USDT:USDT", "15m"),
  38. ("ETH/USDT:USDT", "15m"),
  39. ]
  40. def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
  41. dataframe["eth_return_5m"] = dataframe["close"].pct_change(self.lead_lookback_5m)
  42. if self.dp:
  43. btc_5m = self.dp.get_pair_dataframe(pair="BTC/USDT:USDT", timeframe="5m")
  44. btc_5m["btc_return"] = btc_5m["close"].pct_change(self.lead_lookback_5m)
  45. dataframe = self._merge_informative(dataframe, btc_5m, "btc", "5m")
  46. btc_15m = self.dp.get_pair_dataframe(pair="BTC/USDT:USDT", timeframe="15m")
  47. btc_15m["btc_trend"] = btc_15m["close"].rolling(self.btc_trend_sma).mean()
  48. btc_15m["btc_momentum"] = btc_15m["close"].pct_change(self.btc_momentum_lookback)
  49. btc_15m["btc_return"] = btc_15m["close"].pct_change(self.lead_lookback_15m)
  50. dataframe = self._merge_informative(dataframe, btc_15m, "btc", "15m")
  51. eth_15m = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="15m")
  52. eth_15m["eth_trend"] = eth_15m["close"].rolling(self.eth_rsi_trend_sma).mean()
  53. eth_15m["eth_rsi2"] = self._rsi(eth_15m["close"], self.eth_rsi_length)
  54. eth_15m["eth_return"] = eth_15m["close"].pct_change(self.lead_lookback_15m)
  55. dataframe = self._merge_informative(dataframe, eth_15m, "eth", "15m")
  56. return dataframe
  57. def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
  58. rsi_filter = (
  59. (dataframe["eth_close_15m"] > dataframe["eth_trend_15m"])
  60. & (dataframe["eth_rsi2_15m"] <= self.eth_rsi_threshold)
  61. & (dataframe["btc_close_15m"] > dataframe["btc_trend_15m"])
  62. & (dataframe["btc_momentum_15m"] >= self.btc_min_momentum)
  63. )
  64. lead_lag_15m = (
  65. (dataframe["btc_return_15m"] >= self.btc_return_threshold_15m)
  66. & ((dataframe["btc_return_15m"] - dataframe["eth_return_15m"]) >= self.lag_gap)
  67. )
  68. lead_lag_5m = (
  69. (dataframe["btc_return_5m"] >= self.btc_return_threshold_5m)
  70. & ((dataframe["btc_return_5m"] - dataframe["eth_return_5m"]) >= self.lag_gap)
  71. )
  72. dataframe.loc[rsi_filter, ["enter_long", "enter_tag"]] = (1, "eth_btc_rsi_filter_15m")
  73. dataframe.loc[lead_lag_15m, ["enter_long", "enter_tag"]] = (1, "btc_lead_eth_lag_15m")
  74. dataframe.loc[lead_lag_5m, ["enter_long", "enter_tag"]] = (1, "btc_lead_eth_lag_5m")
  75. return dataframe
  76. def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
  77. dataframe.loc[
  78. (dataframe["eth_rsi2_15m"] >= self.eth_exit_rsi)
  79. | (dataframe["btc_close_15m"] <= dataframe["btc_trend_15m"]),
  80. ["exit_long", "exit_tag"],
  81. ] = (1, "rsi_or_btc_trend_exit")
  82. return dataframe
  83. def custom_exit(
  84. self,
  85. pair: str,
  86. trade: Trade,
  87. current_time: datetime,
  88. current_rate: float,
  89. current_profit: float,
  90. **kwargs,
  91. ) -> str | None:
  92. if trade.enter_tag not in {"btc_lead_eth_lag_15m", "btc_lead_eth_lag_5m"}:
  93. return None
  94. held_bars = int((current_time - trade.open_date_utc).total_seconds() // (5 * 60))
  95. if current_profit <= self.lead_lag_stop_loss:
  96. return "lead_lag_stop"
  97. if current_profit >= self.lead_lag_take_profit:
  98. return "lead_lag_take_profit"
  99. if held_bars >= self.lead_lag_max_hold_bars:
  100. return "lead_lag_max_hold"
  101. return None
  102. def leverage(
  103. self,
  104. pair: str,
  105. current_time: datetime,
  106. current_rate: float,
  107. proposed_leverage: float,
  108. max_leverage: float,
  109. entry_tag: str | None,
  110. side: str,
  111. **kwargs,
  112. ) -> float:
  113. if entry_tag in {"btc_lead_eth_lag_15m", "btc_lead_eth_lag_5m"}:
  114. return min(self.lead_lag_leverage, max_leverage)
  115. return min(self.rsi_filter_leverage, max_leverage)
  116. @staticmethod
  117. def _merge_informative(
  118. dataframe: pd.DataFrame,
  119. informative: pd.DataFrame,
  120. prefix: str,
  121. timeframe: str,
  122. ) -> pd.DataFrame:
  123. minutes = {"5m": 5, "15m": 15}[timeframe]
  124. informative = informative.copy()
  125. informative["merge_date"] = informative["date"] + pd.to_timedelta(minutes, unit="m")
  126. columns = ["merge_date", "open", "high", "low", "close", "volume"]
  127. columns += [column for column in informative.columns if column.startswith(f"{prefix}_")]
  128. informative = informative[columns].rename(
  129. columns={
  130. column: f"{prefix}_{column}_{timeframe}"
  131. for column in columns
  132. if column != "merge_date" and not column.startswith(f"{prefix}_")
  133. }
  134. )
  135. informative = informative.rename(
  136. columns={
  137. column: f"{column}_{timeframe}"
  138. for column in informative.columns
  139. if column.startswith(f"{prefix}_") and not column.endswith(f"_{timeframe}")
  140. }
  141. )
  142. merged = pd.merge_asof(
  143. dataframe.sort_values("date"),
  144. informative.sort_values("merge_date"),
  145. left_on="date",
  146. right_on="merge_date",
  147. direction="backward",
  148. ).ffill()
  149. return merged.drop(columns=[column for column in merged.columns if column.startswith("merge_date")])
  150. @staticmethod
  151. def _rsi(close: pd.Series, length: int) -> pd.Series:
  152. deltas = close.diff()
  153. gains = deltas.clip(lower=0.0)
  154. losses = -deltas.clip(upper=0.0)
  155. values = [float("nan")] * len(close)
  156. if len(close) <= length:
  157. return pd.Series(values, index=close.index)
  158. average_gain = float(gains.iloc[1 : length + 1].mean())
  159. average_loss = float(losses.iloc[1 : length + 1].mean())
  160. for index in range(length, len(close)):
  161. if index > length:
  162. average_gain = ((average_gain * (length - 1)) + float(gains.iloc[index])) / length
  163. average_loss = ((average_loss * (length - 1)) + float(losses.iloc[index])) / length
  164. if pd.isna(average_gain) or pd.isna(average_loss):
  165. continue
  166. if average_loss == 0.0:
  167. values[index] = 100.0 if average_gain > 0.0 else 50.0
  168. continue
  169. relative_strength = average_gain / average_loss
  170. values[index] = 100.0 - (100.0 / (1.0 + relative_strength))
  171. return pd.Series(values, index=close.index)