import numpy as np import pandas as pd import pandas_ta as pta from pandas import DataFrame import talib.abstract as ta from freqtrade.strategy import IStrategy from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter from datetime import datetime import logging logger = logging.getLogger(__name__) class JohnWickSniperStrategy(IStrategy): """ V1 verzija scalping strategije za Freqtrade na 15m timeframe-u. Koristi 5m i 1m kandele za potvrdu trenda, Hammer/Reverse Hammer sveće, Donchian+ADX za LONG, Bollinger+RSI za SHORT. """ # Parametri strategije timeframe = "15m" informative_timeframes = ["5m", "1m"] minimal_roi = {"0": 0.02} # 2% ROI stoploss = -0.015 # Početni fiksni stop-loss trailing_stop = True trailing_stop_positive = 0.01 trailing_stop_positive_offset = 0.015 # Optimizirani parametri donchian_period = IntParameter(10, 30, default=20, space="buy") adx_period = IntParameter(10, 20, default=14, space="buy") adx_threshold = DecimalParameter(20, 40, default=25, space="buy") bb_period = IntParameter(10, 30, default=20, space="sell") bb_std = DecimalParameter(1.5, 3.0, default=2.0, space="sell") rsi_period = IntParameter(10, 20, default=14, space="sell") rsi_sell = DecimalParameter(60, 80, default=70, space="sell") atr_period = IntParameter(10, 20, default=14, space="buy_sell") volume_spike_factor = DecimalParameter(1.5, 3.0, default=2.0, space="buy_sell") # Leverage parametri leverage_num = IntParameter(1, 10, default=3, space="protection") margin_mode = CategoricalParameter(['isolated', 'cross'], default='isolated', space="protection") # Helper funkcija za Heikin Ashi sveće def heikin_ashi(self, dataframe: DataFrame) -> DataFrame: ha_df = DataFrame(index=dataframe.index) ha_df['ha_close'] = (dataframe['open'] + dataframe['high'] + dataframe['low'] + dataframe['close']) / 4 for i in range(len(dataframe)): if i == 0: ha_df.at[i, 'ha_open'] = ((dataframe.at[i, 'open'] + dataframe.at[i, 'close']) / 2) else: ha_df.at[i, 'ha_open'] = ((ha_df.at[i - 1, 'ha_open'] + ha_df.at[i - 1, 'ha_close']) / 2) ha_df['ha_high'] = ha_df[['ha_open', 'ha_close']].join(dataframe['high']).max(axis=1) ha_df['ha_low'] = ha_df[['ha_open', 'ha_close']].join(dataframe['low']).min(axis=1) return ha_df def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # --- KORAK 1: Indikatori na informativnim timeframe-ovima ('5m', '1m') --- for timeframe_inf in self.informative_timeframes: informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=timeframe_inf) if informative.empty: continue ha_informative = self.heikin_ashi(informative) informative['ha_hammer'] = ha_informative.apply( lambda x: 1 if (x['ha_high'] - x['ha_low']) > 3 * abs(x['ha_open'] - x['ha_close']) and (x['ha_close'] - x['ha_low']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 and (x['ha_open'] - x['ha_low']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 else 0, axis=1 ) informative['ha_reverse_hammer'] = ha_informative.apply( lambda x: 1 if (x['ha_high'] - x['ha_low']) > 3 * abs(x['ha_open'] - x['ha_close']) and (x['ha_high'] - x['ha_close']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 and (x['ha_high'] - x['ha_open']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 else 0, axis=1 ) informative['rsi'] = ta.RSI(informative) informative['volume_spike'] = (informative['volume'] > informative['volume'].rolling(20).mean() * 2).astype(int) informative.rename(columns={ 'ha_hammer': f'ha_hammer_{timeframe_inf}', 'ha_reverse_hammer': f'ha_reverse_hammer_{timeframe_inf}', 'rsi': f'rsi_{timeframe_inf}', 'volume_spike': f'volume_spike_{timeframe_inf}' }, inplace=True) dataframe = pd.merge_asof( dataframe, informative[[ 'date', f'ha_hammer_{timeframe_inf}', f'ha_reverse_hammer_{timeframe_inf}', f'rsi_{timeframe_inf}', f'volume_spike_{timeframe_inf}' ]], on='date', direction='backward' ) # --- KORAK 2: Indikatori na glavnom timeframe-u ('15m') --- bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) dataframe['bb_lowerband'] = bollinger['lowerband'] dataframe['bb_middleband'] = bollinger['middleband'] dataframe['bb_upperband'] = bollinger['upperband'] donchian = pta.donchian(high=dataframe['high'], low=dataframe['low'], close=dataframe['close'], length=self.donchian_period.value) upper_col = f'DCU_{self.donchian_period.value}' lower_col = f'DCL_{self.donchian_period.value}' if upper_col in donchian.columns and lower_col in donchian.columns: dataframe['dc_upper'] = donchian[upper_col] dataframe['dc_lower'] = donchian[lower_col] else: dataframe['dc_upper'] = float('NaN') dataframe['dc_lower'] = float('NaN') dataframe['atr'] = ta.ATR(dataframe) # <<< EVO GA DODAT OVDE >>> try: dataframe['adx'] = ta.ADX(dataframe, timeperiod=self.adx_period.value) except Exception: dataframe['adx'] = float('NaN') dataframe['rsi'] = ta.RSI(dataframe) ha_dataframe = self.heikin_ashi(dataframe) dataframe['ha_hammer'] = ha_dataframe.apply(lambda x: 1 if (x['ha_high'] - x['ha_low']) > 3 * abs(x['ha_open'] - x['ha_close']) and (x['ha_close'] - x['ha_low']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 and (x['ha_open'] - x['ha_low']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 else 0, axis=1) dataframe['ha_reverse_hammer'] = ha_dataframe.apply(lambda x: 1 if (x['ha_high'] - x['ha_low']) > 3 * abs(x['ha_open'] - x['ha_close']) and (x['ha_high'] - x['ha_close']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 and (x['ha_high'] - x['ha_open']) / (0.001 + x['ha_high'] - x['ha_low']) > 0.6 else 0, axis=1) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Uslovi za ulaz u LONG i SHORT pozicije. """ # LONG: Breakout + Volume Spike dataframe.loc[ ( (dataframe['close'] > dataframe['dc_upper']) & (dataframe['adx'] > self.adx_threshold.value) & (dataframe['hammer']) & (dataframe['hammer_5m'] | dataframe['hammer_1m']) & (dataframe['volume_spike']) ), 'enter_long'] = 1 # SHORT: Mean Reversion + Volume Spike dataframe.loc[ ( (dataframe['close'] > dataframe['bb_upper']) & (dataframe['rsi'] > self.rsi_sell.value) & (dataframe['reverse_hammer']) & (dataframe['reverse_hammer_5m'] | dataframe['reverse_hammer_1m']) & (dataframe['volume_spike']) ), 'enter_short'] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Uslovi za izlaz iz LONG i SHORT pozicija. """ # Izlaz iz LONG-a dataframe.loc[ ( (dataframe['reverse_hammer']) & (dataframe['reverse_hammer_5m'] | dataframe['reverse_hammer_1m']) ), 'exit_long'] = 1 # Izlaz iz SHORT-a dataframe.loc[ ( (dataframe['hammer']) & (dataframe['hammer_5m'] | dataframe['hammer_1m']) ), 'exit_short'] = 1 return dataframe def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last_candle = dataframe.iloc[-1].squeeze() # Stop loss na osnovu ATR-a # Vrednost se ne ažurira ako je profit veći od neke vrednosti (trailing) if current_profit < 0.1: # npr. ne pomeraj ako je profit > 10% # Uzimamo ATR vrednost iz poslednje sveće stoploss_atr = last_candle['atr'] * 2 # Množilac ATR-a je dobar kandidat za optimizaciju # Postavljamo stop loss ispod cene ulaska za vrednost ATR-a return trade.open_rate - stoploss_atr # Vrati postojeći stop loss ako se uslov ne ispuni return -1 # -1 znači "ne menjaj stop loss" def leverage(self, pair: str, current_time: 'datetime', current_rate: float, proposed_leverage: float, max_leverage: float, side: str, **kwargs) -> float: """Postavlja leverage na 3x.""" return 3.0 def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: 'datetime', **kwargs) -> bool: """Provjerava valjanost ulaza u trejd.""" try: return True except Exception as e: print(f"Greška u confirm_trade_entry: {e}") return False