"""NowoIchimoku5mV2 base strategy (non-FreqAI). Ported from the public freqtrade_pub_strats repository so it can be bundled directly with this project without cloning at runtime. The class name and structure match the original implementation to keep compatibility with wrappers such as FreqAINowoIchimoku5mV2. Note: This strategy depends on TA-Lib, qtpylib, technical and finta packages, which are already included in the Freqtrade GPU image used by docker-compose. """ from __future__ import annotations import math from datetime import datetime from typing import Any, Callable import numpy as np import pandas as pd import talib.abstract as ta from finta import TA from numpy.typing import ArrayLike from pandas import DataFrame, Series import freqtrade.vendor.qtpylib.indicators as qtpylib import technical.indicators as indicators from freqtrade.exchange import timeframe_to_prev_date from freqtrade.persistence import Trade from freqtrade.strategy import DecimalParameter, IStrategy def wma(series: Series, length: int) -> Series: norm = 0 total = 0 for i in range(1, length - 1): weight = (length - i) * length norm += weight total += series.shift(i) * weight return total / norm if norm else series def hma(series: Series, length: int) -> Series: half = wma(series, math.floor(length / 2)) full = wma(series, length) hull = 2 * half - full return wma(hull, math.floor(math.sqrt(length))) def bollinger_bands(series: Series, moving_average: str = "sma", length: int = 20, mult: float = 2.0) -> DataFrame: if moving_average == "sma": basis = ta.SMA(series, length) elif moving_average == "hma": basis = hma(series, length) else: raise ValueError("moving_average has to be 'sma' or 'hma'") dev = mult * ta.STDDEV(series, length) return DataFrame({"upper": basis + dev}) class NowoIchimoku5mV2(IStrategy): timeframe = "5m" startup_candle_count = 100 use_exit_signal = False use_custom_stoploss = True trailing_stop = True minimal_roi = { "0": 0.427, "448": 0.202, "1123": 0.089, "2355": 0, } stoploss = -0.345 plot_config = { "main_plot": { "upper": {"color": "blue"}, }, "subplots": { "Buy Allowed": {"buy_allowed": {"color": "blue"}}, "Should Buy": {"should_buy": {"color": "green"}}, "Is Cloud Green": {"is_cloud_green": {"color": "black"}}, }, } srsi_k_min_profit = DecimalParameter(0.01, 0.99, decimals=3, default=0.036, space="sell") above_upper_min_profit = DecimalParameter(0.001, 0.5, decimals=3, default=0.011, space="sell") limit_factor = DecimalParameter(0.5, 5, decimals=3, default=1.918, space="sell") lower_cloud_factor = DecimalParameter(0.5, 1.5, decimals=3, default=0.971, space="sell") close_above_shifted_upper_cloud = DecimalParameter(0.5, 2, decimals=3, default=0.603, space="buy") def custom_stoploss( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs: Any, ) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if len(dataframe) < 2: return -0.99 last_candle = dataframe.iloc[-1].squeeze() previous_candle = dataframe.iloc[-2].squeeze() if last_candle is None or previous_candle is None: return -0.99 trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) trade_candle = dataframe.loc[dataframe["date"] == trade_date] if trade_candle.empty: return -0.99 trade_candle = trade_candle.squeeze() if last_candle["srsi_k"] > 80 and current_profit > self.srsi_k_min_profit.value: return -0.0001 if ( previous_candle["close"] < previous_candle["upper"] and current_rate > last_candle["upper"] and current_profit > self.above_upper_min_profit.value ): return -0.0001 limit = trade.open_rate + ( (trade.open_rate - trade_candle["shifted_lower_cloud"]) * self.limit_factor.value ) if current_rate > limit: return -0.0001 if current_rate < (trade_candle["shifted_lower_cloud"] * self.lower_cloud_factor.value): return -0.0001 return -0.99 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: df = dataframe.copy() df["upper"] = bollinger_bands(df["close"], moving_average="hma", length=20, mult=2.5)["upper"] ichi = indicators.ichimoku(df) df["conversion_line"] = ichi["tenkan_sen"] df["base_line"] = ichi["kijun_sen"] df["lead_1"] = ichi["leading_senkou_span_a"] df["lead_2"] = ichi["leading_senkou_span_b"] df["cloud_green"] = ichi["cloud_green"] df["upper_cloud"] = df["lead_1"].where(df["lead_1"] > df["lead_2"], df["lead_2"]) df["lower_cloud"] = df["lead_1"].where(df["lead_1"] < df["lead_2"], df["lead_2"]) df["shifted_upper_cloud"] = df["upper_cloud"].shift(25) df["shifted_lower_cloud"] = df["lower_cloud"].shift(25) smooth_k = 3 smooth_d = 3 length_rsi = 14 length_stoch = 14 df["rsi"] = ta.RSI(df, timeperiod=length_rsi) stochrsi = (df["rsi"] - df["rsi"].rolling(length_stoch).min()) / ( df["rsi"].rolling(length_stoch).max() - df["rsi"].rolling(length_stoch).min() ) df["srsi_k"] = stochrsi.rolling(smooth_k).mean() * 100 df["srsi_d"] = df["srsi_k"].rolling(smooth_d).mean() return df def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: df = dataframe.copy() df["is_cloud_green"] = df["lead_1"] > df["lead_2"] double_shifted_upper_cloud = df["upper_cloud"].shift(50) close_above_shifted_upper = df["close"] > df["shifted_upper_cloud"] * self.close_above_shifted_upper_cloud.value close_above_shifted_lower = df["close"] > df["shifted_lower_cloud"] close_above_double_shifted_upper = df["close"] > double_shifted_upper_cloud conversion_above_base = df["conversion_line"] > df["base_line"] close_above_shifted_conversion = df["close"] > df["conversion_line"].shift(25) df["should_buy"] = ( (df["close"] > df["open"]) & close_above_shifted_upper & close_above_shifted_lower & df["is_cloud_green"] & conversion_above_base & close_above_shifted_conversion & close_above_double_shifted_upper ) df["buy"] = False df["buy_allowed"] = True for row in df.itertuples(): if row.Index <= 100: continue df.loc[row.Index, "buy_allowed"] = df.at[row.Index - 1, "buy_allowed"] if df.at[row.Index - 1, "buy"]: df.loc[row.Index, "buy_allowed"] = False if not df.at[row.Index, "is_cloud_green"]: df.loc[row.Index, "buy_allowed"] = True df.loc[row.Index, "buy"] = df.at[row.Index, "buy_allowed"] & df.at[row.Index, "should_buy"] return df def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: df = dataframe.copy() df["sell"] = 0 return df