From 4c0692426eefbfb50fe6a1579f35eefe02025da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Delacotte?= Date: Mon, 20 Oct 2025 20:54:24 +0200 Subject: [PATCH] TEST Simple --- Simple.py | 265 +++++++++++++++++++++ Simple_01.py | 633 +++++++++++++++++++++++++++++++++++++++++++++++++++ Zeus_8_1d.py | 85 +++++-- 3 files changed, 963 insertions(+), 20 deletions(-) create mode 100644 Simple.py create mode 100644 Simple_01.py diff --git a/Simple.py b/Simple.py new file mode 100644 index 0000000..fce51e9 --- /dev/null +++ b/Simple.py @@ -0,0 +1,265 @@ +# Zeus Strategy: First Generation of GodStra Strategy with maximum +# AVG/MID profit in USDT +# Author: @Mablue (Masoud Azizi) +# github: https://github.com/mablue/ +# IMPORTANT: INSTALL TA BEFOUR RUN(pip install ta) +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --spaces buy sell roi --strategy Zeus +# --- Do not remove these libs --- +from datetime import timedelta, datetime +from freqtrade.persistence import Trade +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, stoploss_from_open, + IntParameter, IStrategy, merge_informative_pair, informative, stoploss_from_absolute) +import pandas as pd +import numpy as np +from pandas import DataFrame +from typing import Optional, Union, Tuple +from typing import List + +import logging +import configparser +from technical import pivots_points +# -------------------------------- + +# Add your lib to import here test git +import ta +import talib.abstract as talib +import freqtrade.vendor.qtpylib.indicators as qtpylib +import requests +from datetime import timezone, timedelta +from scipy.signal import savgol_filter +from ta.trend import SMAIndicator, EMAIndicator, MACD, ADXIndicator +from collections import Counter + +logger = logging.getLogger(__name__) + +from tabulate import tabulate + +# Couleurs ANSI de base +RED = "\033[31m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +BLUE = "\033[34m" +MAGENTA = "\033[35m" +CYAN = "\033[36m" +RESET = "\033[0m" + + +def pprint_df(dframe): + print(tabulate(dframe, headers='keys', tablefmt='psql', showindex=False)) + + +def normalize(df): + df = (df - df.min()) / (df.max() - df.min()) + return df + +""" +SMA z-score derivative strategy with trailing exit (large). +- timeframe: 1h +- sma5, relative derivatives, z-score normalization (rolling z over z_window) +- smoothing: ewm(span=5) on z-scores +- entry: z_d1_smooth > entry_z1 AND z_d2_smooth > entry_z2 +- exit: inversion (z_d1_smooth < 0) AND retrace from highest since entry >= trailing_stop +""" +class Simple(IStrategy): + levels = [1, 2, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20] + startup_candle_count = 12 * 24 * 2 + + # ROI table: + minimal_roi = { + "0": 10 + } + stakes = 40 + + # Stoploss: + stoploss = -1 # 0.256 + # Custom stoploss + use_custom_stoploss = True + + # Buy hypers + timeframe = '1h' + + max_open_trades = 5 + max_amount = 40 + + # DCA config + position_adjustment_enable = True + + # Parameters (tweakable) + z_window = 50 # window for rolling mean/std to compute zscore + entry_z1 = 0.1 # threshold on z-score of first derivative + entry_z2 = 0.1 # threshold on z-score of second derivative + min_volume = 0.0 # minimal volume to accept an entry + min_relative_d1 = 1e-6 # clip tiny d1 relative values to reduce noise + + # Trailing parameters for "large" trailing requested + trailing_stop_pct = 0.05 # 5% retracement from highest since entry + + # Smoothing for z-scores + ewm_span = 5 + + # Plot config: price + sma5 + markers + slope/accel subplots + plot_config = { + "main_plot": { + "close": {"color": "blue"}, + "sma5": {"color": "orange"}, + }, + "subplots": { + "slope_and_accel": { + "z_d1": {"color": "green"}, + "z_d2": {"color": "red"}, + } + }, + # Markers (Freqtrade charting supports markers via these keys) + "markers": [ + # buy marker: '^' green when enter_long==1 + {"type": "buy", "column": "enter_long", "marker": "^", "color": "green", "markersize": 10}, + # sell marker: 'v' red when exit_long==1 + {"type": "sell", "column": "exit_long", "marker": "v", "color": "red", "markersize": 10}, + ], + } + + def informative_pairs(self): + return [] + + def _zscore(self, series: pd.Series, window: int) -> pd.Series: + mean = series.rolling(window=window, min_periods=3).mean() + std = series.rolling(window=window, min_periods=3).std(ddof=0) + z = (series - mean) / std + z = z.replace([np.inf, -np.inf], np.nan) + return z + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + df = dataframe + + # SMA(5) + df['sma5'] = df['close'].rolling(5, min_periods=1).mean() + + # Absolute derivatives + df['sma5_d1'] = df['sma5'].diff().rolling(5).mean() + df['sma5_d2'] = df['sma5_d1'].diff().rolling(5).mean() + + # Relative derivatives (percentage-like) + eps = 1e-9 + df['sma5_d1_rel'] = df['sma5_d1'] / (df['sma5'].shift(1).replace(0, np.nan) + eps) + df['sma5_d2_rel'] = df['sma5_d2'] / (df['sma5'].shift(2).replace(0, np.nan) + eps) + + # Clip micro-noise + df.loc[df['sma5_d1_rel'].abs() < self.min_relative_d1, 'sma5_d1_rel'] = 0.0 + df.loc[df['sma5_d2_rel'].abs() < self.min_relative_d1, 'sma5_d2_rel'] = 0.0 + + # Z-scores on relative derivatives + df['z_d1'] = self._zscore(df['sma5_d1_rel'], self.z_window) + df['z_d2'] = self._zscore(df['sma5_d2_rel'], self.z_window) + + # Smoothing z-scores with EWM to reduce jitter + df['z_d1_smooth'] = df['z_d1'].ewm(span=self.ewm_span, adjust=False).mean() + df['z_d2_smooth'] = df['z_d2'].ewm(span=self.ewm_span, adjust=False).mean() + + # Prepare marker columns (for plots). They will be filled in populate_entry_trend/populate_exit_trend + df['enter_long'] = 0 + df['exit_long'] = 0 + + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + df = dataframe.copy() + + # Use last closed candle for signals -> shift(1) + cond_entry = ( + (df['z_d1'] > self.entry_z1) & + (df['z_d2'] > self.entry_z2) & + (df['volume'].shift(1) > self.min_volume) + ) + + df.loc[cond_entry, 'enter_long'] = 1 + # Ensure others are explicitly zero (for clean plotting) + df.loc[~cond_entry, 'enter_long'] = 0 + + return df + + def custom_exit(self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs) -> \ + Optional[str]: + """ + Exit policy (mode C - trailing large): + - Must detect inversion: z_d1_smooth < 0 on last closed candle + - Compute highest close since trade entry (inclusive) + - If price has retraced >= trailing_stop_pct from that highest point and we're in inversion -> exit + """ + + df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last = df.iloc[-1].squeeze() + + z1 = last.get('z_d1_smooth', None) + if z1 is None: + return None + + # Only consider exits when inversion detected (z1 < 0) + inversion = (z1 < 0) + + if not inversion: + return None + + # If we don't have profit info, be conservative + if current_profit is None: + return None + + # Determine highest close since entry + highest_since_entry = None + try: + # trade.open_date_utc is available: find rows after that timestamp + # df.index is expected to be pd.DatetimeIndex in UTC or local; convert safely + if hasattr(trade, 'open_date_utc') and trade.open_date_utc: + # pandas comparison: ensure same tz awareness + entry_time = pd.to_datetime(trade.open_date_utc) + # select rows with index >= entry_time + mask = df.index >= entry_time + if mask.any(): + highest_since_entry = df.loc[mask, 'close'].max() + # fallback: use trade.open_rate or the max over full df + except Exception as e: + logger.debug(f"Couldn't compute highest_since_entry from open_date_utc: {e}") + + if highest_since_entry is None: + # fallback: use the maximum close in the entire provided dataframe slice + highest_since_entry = df['close'].max() if not df['close'].empty else current_rate + + # Calculate retracement ratio from the highest + if highest_since_entry and highest_since_entry > 0: + retrace = 1.0 - (current_rate / highest_since_entry) + else: + retrace = 0.0 + + # Exit if: + # - currently in profit AND + # - retracement >= trailing_stop_pct (i.e. price has fallen enough from top since entry) AND + # - inversion detected (z1 < 0) + if (current_profit > 0) and (retrace >= self.trailing_stop_pct): + # Mark the dataframe for plotting (if possible) + # Note: freqtrade expects strategies to set exit flags in populate_exit_trend, + # but we set exit via custom_exit return reason; plotting will read exit_long if populated. + return "zscore" + + # Otherwise, do not exit yet + return None + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + For plotting only: mark exit points where our logic would trigger. + This is approximate: we mark an exit when z_d1_smooth < 0 and the price has retraced >= trailing_stop_pct + based on the available dataframe window (best-effort). + """ + df = dataframe + # df['exit_long'] = 0 + # + # # compute highest close since each possible entry (best-effort: use rolling max up to current index) + # rolling_max = df['close'].cummax() + # + # # retracement relative to rolling max + # retrace = 1.0 - (df['close'] / rolling_max.replace(0, np.nan)) + # + # # mark exits where inversion and retrace >= threshold + # cond_exit = (df['z_d1_smooth'] < 0) & (retrace >= self.trailing_stop_pct) + # # shift by 0: we mark the candle where the exit condition appears + # df.loc[cond_exit, 'exit_long'] = 1 + + return df diff --git a/Simple_01.py b/Simple_01.py new file mode 100644 index 0000000..5a44c4c --- /dev/null +++ b/Simple_01.py @@ -0,0 +1,633 @@ +# sma_zscore_trailing_dca.py +from freqtrade.strategy.interface import IStrategy +from freqtrade.persistence import Trade +from pandas import DataFrame +import pandas as pd +import numpy as np +from typing import Optional +import logging +from datetime import timedelta, datetime +import talib.abstract as talib + +logger = logging.getLogger(__name__) + +class Simple_01(IStrategy): + """ + SMA z-score derivative strategy with trailing exit and intelligent DCA (large pullback). + - timeframe: 1h + - sma5, relative derivatives, z-score normalization (rolling z over z_window) + - smoothing: ewm(span=5) on z-scores + - entry: z_d1_smooth > entry_z1 AND z_d2_smooth > entry_z2 + - exit: inversion (z_d1_smooth < 0) AND retrace from highest since entry >= trailing_stop_pct + - adjust_trade_position: add to position on controlled pullback (large = 4-6%) + """ + + timeframe = "1h" + startup_candle_count = 24 + # Risk mgmt (we handle exit in custom_exit) + minimal_roi = {"0": 0.99} + stoploss = -0.99 + use_custom_exit = True + position_adjustment_enable = True + + columns_logged = False + + # Parameters + z_window = 10 # window for rolling mean/std to compute zscore + entry_z1 = 0.1 # threshold on z-score of first derivative + entry_z2 = 0 # threshold on z-score of second derivative + min_volume = 0.0 # minimal volume to accept an entry + min_relative_d1 = 1e-6 # clip tiny d1 relative values to reduce noise + + # Trailing parameters for "large" trailing (exit) + trailing_stop_pct = 0.01 # 5% retracement from highest since entry + + # Smoothing for z-scores + ewm_span = 5 + + # DCA intelligent (adjust_trade_position) parameters for "large" pullback + dca_enabled = True + # Pullback bounds (large): allow adding when retrace is between 4% and 6% + dca_pullback_min = 0.01 + dca_pullback_max = 0.02 + # Maximum number of adds per trade (to avoid infinite pyramiding) + dca_max_adds = 8 + # Percentage of base position to add on each reinforcement (50% of original size by default) + dca_add_ratio = 0.5 + # Require momentum still positive to add + dca_require_z1_positive = True + dca_require_z2_positive = True + # Do not add if current_profit < min_profit_to_add (avoid averaging down when deep in loss) + min_profit_to_add = -0.02 # allow small loss but not big drawdown + + pairs = { + pair: { + "first_buy": 0, + "last_buy": 0.0, + "first_amount": 0.0, + "last_min": 999999999999999.5, + "last_max": 0, + "trade_info": {}, + "max_touch": 0.0, + "last_sell": 0.0, + 'count_of_buys': 0, + 'current_profit': 0, + 'expected_profit': 0, + "last_candle": {}, + "last_trade": None, + "last_count_of_buys": 0, + 'base_stake_amount': 0, + 'stop_buy': False, + 'last_date': 0, + 'stop': False, + 'max_profit': 0, + 'last_palier_index': -1, + 'total_amount': 0, + 'has_gain': 0, + 'force_sell': False, + 'force_buy': False + } + for pair in ["BTC/USDC", "ETH/USDC", "DOGE/USDC", "XRP/USDC", "SOL/USDC", + "BTC/USDT", "ETH/USDT", "DOGE/USDT", "XRP/USDT", "SOL/USDT"] + } + + # Plot config + plot_config = { + "main_plot": { + "close": {"color": "blue"}, + "sma5": {"color": "orange"}, + }, + "subplots": { + "slope_and_accel": { + "z_d1_smooth": {"color": "green"}, + "z_d2_smooth": {"color": "red"}, + }, + "sma_deriv": { + "sma5_deriv1_rel": {"color": "green"}, + "sma5_deriv2_rel": {"color": "red"}, + }, + "rsi": { + "rsi": {"color": "blue"} + } + + }, + "markers": [ + {"type": "buy", "column": "enter_long", "marker": "^", "color": "green", "markersize": 10}, + {"type": "sell", "column": "exit_long", "marker": "v", "color": "red", "markersize": 10}, + ], + } + + def informative_pairs(self): + return [] + + def _zscore(self, series: pd.Series, window: int) -> pd.Series: + mean = series.rolling(window=window, min_periods=3).mean() + std = series.rolling(window=window, min_periods=3).std(ddof=0) + z = (series - mean) / std + z = z.replace([np.inf, -np.inf], np.nan) + return z + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + df = dataframe + + # SMA(5) + df['sma5'] = df['close'].rolling(5, min_periods=1).mean() + + # Absolute derivatives + df['sma5_deriv1'] = df['sma5'].diff() + df['sma5_deriv2'] = df['sma5_deriv1'].diff() + + # Relative derivatives (percentage-like) + eps = 1e-9 + df['sma5_deriv1_rel'] = df['sma5_deriv1'] / (df['sma5'].shift(1).replace(0, np.nan) + eps) + df['sma5_deriv2_rel'] = df['sma5_deriv2'] / (df['sma5'].shift(2).replace(0, np.nan) + eps) + + # Clip micro-noise + df.loc[df['sma5_deriv1_rel'].abs() < self.min_relative_d1, 'sma5_deriv1_rel'] = 0.0 + df.loc[df['sma5_deriv2_rel'].abs() < self.min_relative_d1, 'sma5_deriv2_rel'] = 0.0 + + # Z-scores on relative derivatives + df['z_d1'] = self._zscore(df['sma5_deriv1_rel'], self.z_window) + df['z_d2'] = self._zscore(df['sma5_deriv2_rel'], self.z_window) + + # Smoothing z-scores with EWM to reduce jitter + df['z_d1_smooth'] = df['z_d1'].ewm(span=self.ewm_span, adjust=False).mean() + df['z_d2_smooth'] = df['z_d2'].ewm(span=self.ewm_span, adjust=False).mean() + + # Prepare marker columns (for plots) + df['enter_long'] = 0 + df['exit_long'] = 0 + df['rsi'] = talib.RSI(df['close'], timeperiod=14) + df['max_rsi_12'] = talib.MAX(df['rsi'], timeperiod=12) + df['min_rsi_12'] = talib.MIN(df['rsi'], timeperiod=12) + # self.calculeDerivees(df, 'rsi', horizon=12) + + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + df = dataframe + + # Use last closed candle for signals -> shift(1) + cond_entry = ( + (df['z_d1_smooth'] > self.entry_z1) & + (df['z_d2_smooth'] > self.entry_z2) & + (df['max_rsi_12'] < 70) & + (df['volume'] > self.min_volume) + ) + + df.loc[cond_entry, 'enter_long'] = 1 + df.loc[~cond_entry, 'enter_long'] = 0 + + return df + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + df = dataframe + # df['exit_long'] = 0 + # + # # compute rolling max (best-effort for plotting) + # rolling_max = df['close'].cummax() + # retrace = 1.0 - (df['close'] / rolling_max.replace(0, np.nan)) + # + # cond_exit = (df['z_d1_smooth'] < 0) & (retrace >= self.trailing_stop_pct) + # df.loc[cond_exit, 'exit_long'] = 1 + + return df + + 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, **kwargs, ) -> bool: + # allow_to_sell = (minutes > 30) + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + force = self.pairs[pair]['force_sell'] + allow_to_sell = True #(last_candle['percent'] < 0) #or force + + minutes = int(round((current_time - trade.date_last_filled_utc).total_seconds() / 60, 0)) + + if allow_to_sell: + self.trades = list() + self.pairs[pair]['last_count_of_buys'] = trade.nr_of_successful_entries # self.pairs[pair]['count_of_buys'] + self.pairs[pair]['last_sell'] = rate + self.pairs[pair]['last_trade'] = trade + self.pairs[pair]['last_candle'] = last_candle + self.trades = list() + dispo = round(self.wallets.get_available_stake_amount()) + print(f"Sell {pair} {current_time} {exit_reason} dispo={dispo} amount={amount} rate={rate} open_rate={trade.open_rate}") + self.log_trade( + last_candle=last_candle, + date=current_time, + action="🟥Sell " + str(minutes), + pair=pair, + trade_type=exit_reason, + rate=last_candle['close'], + dispo=dispo, + profit=round(trade.calc_profit(rate, amount), 2) + ) + self.pairs[pair]['max_profit'] = 0 + self.pairs[pair]['force_sell'] = False + self.pairs[pair]['has_gain'] = 0 + self.pairs[pair]['current_profit'] = 0 + self.pairs[pair]['total_amount'] = 0 + self.pairs[pair]['count_of_buys'] = 0 + self.pairs[pair]['max_touch'] = 0 + self.pairs[pair]['last_buy'] = 0 + self.pairs[pair]['last_date'] = current_time + self.pairs[pair]['last_palier_index'] = -1 + self.pairs[pair]['last_trade'] = trade + self.pairs[pair]['current_trade'] = None + + return (allow_to_sell) | (exit_reason == 'force_exit') + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, + current_time: datetime, entry_tag: Optional[str], **kwargs) -> bool: + + minutes = 0 + if self.pairs[pair]['last_date'] != 0: + minutes = round(int((current_time - self.pairs[pair]['last_date']).total_seconds() / 60)) + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + last_candle_2 = dataframe.iloc[-2].squeeze() + last_candle_3 = dataframe.iloc[-3].squeeze() + # val = self.getProbaHausse144(last_candle) + + # allow_to_buy = True #(not self.stop_all) #& (not self.all_down) + allow_to_buy = not self.pairs[pair]['stop'] # and val > self.buy_val.value #not last_candle['tendency'] in ('B-', 'B--') # (rate <= float(limit)) | (entry_tag == 'force_entry') + + # force = self.pairs[pair]['force_buy'] + # if self.pairs[pair]['force_buy']: + # self.pairs[pair]['force_buy'] = False + # allow_to_buy = True + # else: + # if not self.should_enter_trade(pair, last_candle, current_time): + # allow_to_buy = False + + if allow_to_buy: + self.trades = list() + self.pairs[pair]['first_buy'] = rate + self.pairs[pair]['last_buy'] = rate + self.pairs[pair]['max_touch'] = last_candle['close'] + self.pairs[pair]['last_candle'] = last_candle + self.pairs[pair]['count_of_buys'] = 1 + self.pairs[pair]['current_profit'] = 0 + self.pairs[pair]['last_palier_index'] = -1 + self.pairs[pair]['last_max'] = max(last_candle['close'], self.pairs[pair]['last_max']) + self.pairs[pair]['last_min'] = min(last_candle['close'], self.pairs[pair]['last_min']) + + dispo = round(self.wallets.get_available_stake_amount()) + self.printLineLog() + + stake_amount = self.adjust_stake_amount(pair, last_candle) + + self.pairs[pair]['total_amount'] = stake_amount + + self.log_trade( + last_candle=last_candle, + date=current_time, + action=("🟩Buy" if allow_to_buy else "Canceled") + " " + str(minutes), + pair=pair, + rate=rate, + dispo=dispo, + profit=0, + trade_type=entry_tag, + buys=1, + stake=round(stake_amount, 2) + ) + + return allow_to_buy + + def custom_exit(self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs) -> Optional[str]: + df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = df.iloc[-1].squeeze() + + self.pairs[pair]['last_max'] = max(last_candle['close'], self.pairs[pair]['last_max']) + self.pairs[pair]['last_min'] = min(last_candle['close'], self.pairs[pair]['last_min']) + self.pairs[pair]['current_trade'] = trade + count_of_buys = trade.nr_of_successful_entries + + profit = round(current_profit * trade.stake_amount, 1) + self.pairs[pair]['max_profit'] = max(self.pairs[pair]['max_profit'], profit) + max_profit = self.pairs[pair]['max_profit'] + baisse = 0 + if profit > 0: + baisse = 100 * abs(max_profit - profit) / max_profit + mx = max_profit / 5 + self.pairs[pair]['count_of_buys'] = count_of_buys + self.pairs[pair]['current_profit'] = profit + + dispo = round(self.wallets.get_available_stake_amount()) + hours_since_first_buy = (current_time - trade.open_date_utc).seconds / 3600.0 + days_since_first_buy = (current_time - trade.open_date_utc).days + hours = (current_time - trade.date_last_filled_utc).total_seconds() / 3600.0 + + if hours % 4 == 0: + self.log_trade( + last_candle=last_candle, + date=current_time, + action="🔴 CURRENT" if self.pairs[pair]['stop'] else "🟢 CURRENT", + dispo=dispo, + pair=pair, + rate=last_candle['close'], + trade_type='', + profit=profit, + buys='', + stake=0 + ) + + z1 = last_candle.get('z_d1_smooth', None) + if z1 is None: + return None + + if z1 >= 0: + return None + + if current_profit is None: + return None + + # highest close since entry + highest_since_entry = self.pairs[trade.pair]['max_touch'] + # try: + # if hasattr(trade, 'open_date_utc') and trade.open_date_utc: + # entry_time = pd.to_datetime(trade.open_date_utc) + # mask = df.index >= entry_time + # if mask.any(): + # highest_since_entry = df.loc[mask, 'close'].max() + # except Exception as e: + # logger.debug(f"Couldn't compute highest_since_entry: {e}") + # + # if highest_since_entry is None: + # highest_since_entry = df['close'].max() if not df['close'].empty else current_rate + + if highest_since_entry and highest_since_entry > 0: + retrace = 1.0 - (current_rate / highest_since_entry) + else: + retrace = 0.0 + z1 = last_candle.get('z_d1_smooth', None) + z2 = last_candle.get('z_d2_smooth', None) + + if (current_profit > 0) and (current_profit >= self.trailing_stop_pct) and last_candle['sma5_deriv1'] < -0.002: + return str(count_of_buys) + '_' + "zscore" + + self.pairs[pair]['max_touch'] = max(last_candle['close'], self.pairs[pair]['max_touch']) + + return None + + def adjust_stake_amount(self, pair: str, last_candle: DataFrame): + # Calculer le minimum des 14 derniers jours + return self.config.get('stake_amount') + + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, min_stake: float, + max_stake: float, **kwargs): + """ + DCA intelligent (mode C - 'large'): + - Only add if: + * DCA is enabled + * number of adds done so far < dca_max_adds + * retracement since highest_since_entry is between dca_pullback_min and dca_pullback_max + * momentum still positive (z_d1_smooth, z_d2_smooth) if required + * current_profit >= min_profit_to_add (avoid averaging down into large loss) + - Returns a dict describing the desired order for Freqtrade to place (common format). + Example returned dict: {'type': 'market', 'amount': 0.01} + """ + if not self.dca_enabled: + return None + pair = trade.pair + + # Basic guards + if current_profit is None: + current_profit = 0.0 + + # Do not add if we're already deeply in loss + if current_profit < self.min_profit_to_add: + return None + + df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = df.iloc[-1].squeeze() + + # Compute highest close since entry + highest_since_entry = self.pairs[trade.pair]['last_buy'] + + last = df.iloc[-1] + z1 = last.get('z_d1_smooth', None) + z2 = last.get('z_d2_smooth', None) + + # Count how many adds have been done for this trade. + # Trade.extra might contain meta; otherwise try trade.tags or use trade.open_rate/amount heuristics. + adds_done = trade.nr_of_successful_entries + # Calculate retracement since the highest + retrace = 0.0 + if highest_since_entry and highest_since_entry > 0: + retrace = (last['close'] - highest_since_entry) / highest_since_entry + + # logger.info(f"{pair} {current_rate} {current_time} {highest_since_entry} add: retrace={retrace:.4f}, adds_done={adds_done} z1={z1} z2={z2}") + + # Enforce momentum requirements if requested + if self.dca_require_z1_positive and (z1 is None or z1 <= 0): + return None + if self.dca_require_z2_positive and (z2 is None or z2 <= 0): + return None + + # + # try: + # meta = getattr(trade, 'meta', None) or {} + # adds_done = int(meta.get('adds_done', 0)) + # except Exception: + # adds_done = 0 + + if adds_done >= self.dca_max_adds: + return None + + # try: + # if hasattr(trade, 'open_date_utc') and trade.open_date_utc: + # entry_time = pd.to_datetime(trade.open_date_utc) + # mask = df.index >= entry_time + # if mask.any(): + # highest_since_entry = df.loc[mask, 'close'].max() + # except Exception as e: + # logger.debug(f"adjust_trade_position: couldn't compute highest_since_entry: {e}") + # + # if highest_since_entry is None: + # highest_since_entry = df['close'].max() if not df['close'].empty else current_rate + + # Check if retrace is inside the allowed DCA window (large) + if ((retrace >= self.dca_pullback_min) and (retrace <= self.dca_pullback_max)): + # Determine amount to add: a fraction of the original trade amount + # Try to get trade.amount (base asset amount). If not available, fall back to stake percentage + add_amount = None + try: + base_amount = self.config.get('stake_amount') + if base_amount: + add_amount = base_amount * self.dca_add_ratio + else: + # Fallback: attempt to compute amount from trade.open_rate and desired quote stake + # We'll propose to use a fraction of current rate worth (this is best-effort) + add_amount = None + except Exception: + add_amount = None + + # If we couldn't compute an absolute amount, propose a relative size via a suggested stake (user must map) + if add_amount is None: + # Return a suggested instruction; adapt according to your freqtrade version. + suggested = { + 'type': 'market', + 'amount': None, # caller should compute actual amount from stake management + 'note': f'suggest_add_ratio={self.dca_add_ratio}' + } + # logger.info(f"{pair} {current_rate} DCA suggestion (no absolute amount): retrace={retrace:.4f}, adds_done={adds_done}") + return None + + dispo = round(self.wallets.get_available_stake_amount()) + trade_type = last_candle['enter_tag'] if last_candle['enter_long'] == 1 else 'pct48' + self.pairs[trade.pair]['count_of_buys'] += 1 + self.pairs[pair]['total_amount'] += add_amount + self.log_trade( + last_candle=last_candle, + date=current_time, + action="🟧 Loss -", + dispo=dispo, + pair=trade.pair, + rate=current_rate, + trade_type=trade_type, + profit=round(current_profit * trade.stake_amount, 1), + buys=trade.nr_of_successful_entries + 1, + stake=round(add_amount, 2) + ) + + self.pairs[trade.pair]['last_buy'] = current_rate + self.pairs[trade.pair]['max_touch'] = last_candle['close'] + self.pairs[trade.pair]['last_candle'] = last_candle + + # All checks passed -> create market order instruction + # logger.info(f"{pair} {current_rate} {current_time} {highest_since_entry} add: retrace={retrace:.4f}, adds_done={adds_done}, add_amount={add_amount:.8f}") + return add_amount + + # Not in allowed retrace window -> no action + return None + + def getPctFirstBuy(self, pair, last_candle): + return round((last_candle['close'] - self.pairs[pair]['first_buy']) / self.pairs[pair]['first_buy'], 3) + + def getPctLastBuy(self, pair, last_candle): + return round((last_candle['close'] - self.pairs[pair]['last_buy']) / self.pairs[pair]['last_buy'], 4) + + def getPct60D(self, pair, last_candle): + return round((last_candle['max60'] - last_candle['min60']) / last_candle['max60'], 4) + + def getPctClose60D(self, pair, last_candle): + if last_candle['close'] > last_candle['max12']: + return 1 + if last_candle['close'] < last_candle['min12']: + return 0 + return round( + (last_candle['close'] - last_candle['min12']) / (last_candle['max12'] - last_candle['min12']), 4) + + + def printLineLog(self): + self.printLog( + f"+{'-' * 18}+{'-' * 12}+{'-' * 5}+{'-' * 20}+{'-' * 9}+{'-' * 8}+{'-' * 12}+{'-' * 8}+{'-' * 13}+{'-' * 14}+{'-' * 9}{'-' * 9}+{'-' * 5}+{'-' * 7}+" + f"{'-' * 3}" + # "+{'-' * 3}+{'-' * 3} + f"+{'-' * 6}+{'-' * 7}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+" + ) + + def printLog(self, str): + if self.config.get('runmode') == 'hyperopt' or self.dp.runmode.value in ('hyperopt'): + return + if not self.dp.runmode.value in ('backtest', 'hyperopt', 'lookahead-analysis'): + logger.info(str) + else: + if not self.dp.runmode.value in ('hyperopt'): + print(str) + + def log_trade(self, action, pair, date, trade_type=None, rate=None, dispo=None, profit=None, buys=None, + stake=None, + last_candle=None): + # Couleurs ANSI de base + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + MAGENTA = "\033[35m" + CYAN = "\033[36m" + RESET = "\033[0m" + + # Afficher les colonnes une seule fois + if self.config.get('runmode') == 'hyperopt' or self.dp.runmode.value in ('hyperopt'): + return + if self.columns_logged % 10 == 0: + self.printLog( + f"| {'Date':<16} | {'Action':<10} |{'Pair':<5}| {'Trade Type':<18} |{'Rate':>8} | {'Dispo':>6} | {'Profit':>8} | {'Pct':>6} | {'max_touch':>11} | {'last_lost':>12} | {'last_max':>7}| {'last_max':>7}|{'Buys':>5}| {'Stake':>5} |" + f"Tdc|{'val':>6}| RSI |s201d|s5_1d|s5_2d|s51h|s52h" + ) + self.printLineLog() + df = pd.DataFrame.from_dict(self.pairs, orient='index') + colonnes_a_exclure = ['last_candle', 'last_trade', 'last_palier_index', + 'trade_info', 'last_date', 'expected_profit', 'last_count_of_buys', + 'base_stake_amount', 'stop_buy'] + df_filtered = df[df['count_of_buys'] > 0].drop(columns=colonnes_a_exclure) + # df_filtered = df_filtered["first_buy", "last_max", "max_touch", "last_sell","last_buy", 'count_of_buys', 'current_profit'] + + print(df_filtered) + + self.columns_logged += 1 + date = str(date)[:16] if date else "-" + limit = None + # if buys is not None: + # limit = round(last_rate * (1 - self.fibo[buys] / 100), 4) + + rsi = '' + rsi_pct = '' + sma5_1d = '' + sma5_1h = '' + + sma5 = str(sma5_1d) + ' ' + str(sma5_1h) + + last_lost = '' #self.getLastLost(last_candle, pair) + + if buys is None: + buys = '' + + max_touch = '' # round(last_candle['max12'], 1) #round(self.pairs[pair]['max_touch'], 1) + pct_max = '' #self.getPctFirstBuy(pair, last_candle) + + total_counts = str(buys) + '/' + str( + sum(pair_data['count_of_buys'] for pair_data in self.pairs.values())) + + dist_max = '' #self.getDistMax(last_candle, pair) + val = 0 #self.getProbaHausseSma5d(last_candle) + + pct60 = 0 #round(100 * self.getPct60D(pair, last_candle), 2) + + color = GREEN if profit > 0 else RED + # color_sma20 = GREEN if last_candle['sma20_deriv1'] > 0 else RED + # color_sma5 = GREEN if last_candle['mid_smooth_5_deriv1'] > 0 else RED + # color_sma5_2 = GREEN if last_candle['mid_smooth_5_deriv2'] > 0 else RED + # color_sma5_1h = GREEN if last_candle['sma5_deriveriv1'] > 0 else RED + # color_sma5_2h = GREEN if last_candle['sma5_deriveriv2'] > 0 else RED + + last_max = int(self.pairs[pair]['last_max']) if self.pairs[pair]['last_max'] > 1 else round( + self.pairs[pair]['last_max'], 3) + last_min = int(self.pairs[pair]['last_min']) if self.pairs[pair]['last_min'] > 1 else round( + self.pairs[pair]['last_min'], 3) + + profit = str(profit) + '/' + str(round(self.pairs[pair]['max_profit'], 2)) + + # 🟢 Dérivée 1 > 0 et dérivée 2 > 0: tendance haussière qui s’accélère. + # 🟡 Dérivée 1 > 0 et dérivée 2 < 0: tendance haussière qui ralentit → essoufflement potentiel. + # 🔴 Dérivée 1 < 0 et dérivée 2 < 0: tendance baissière qui s’accélère. + # 🟠 Dérivée 1 < 0 et dérivée 2 > 0: tendance baissière qui ralentit → possible bottom. + + # tdc last_candle['tendency_12'] + self.printLog( + f"| {date:<16} |{action:<10} | {pair[0:3]:<3} | {trade_type or '-':<18} |{rate or '-':>9}| {dispo or '-':>6} " + f"|{color}{profit or '-':>10}{RESET}| {pct_max or '-':>6} | {round(self.pairs[pair]['max_touch'], 2) or '-':>11} | {last_lost or '-':>12} " + f"| {last_max or '-':>7} | {last_min or '-':>7} |{total_counts or '-':>5}|{stake or '-':>7}" + f"|{'-':>3}|" + f"{round(val, 1) or '-' :>6}|" + # f"{round(last_candle['rsi'], 0):>7}|{color_sma20}{round(last_candle['sma20_deriv1'], 2):>5}{RESET}" + # f"|{color_sma5}{round(last_candle['mid_smooth_5_deriv1'], 2):>5}{RESET}|{color_sma5_2}{round(last_candle['mid_smooth_5_deriv2'], 2):>5}{RESET}" + # f"|{color_sma5_1h}{round(last_candle['sma5_deriveriv1'], 2):>5}{RESET}|{color_sma5_2h}{round(last_candle['sma5_deriveriv2'], 2):>5}{RESET}" + # f"|{last_candle['min60']}|{last_candle['max60']}" + ) \ No newline at end of file diff --git a/Zeus_8_1d.py b/Zeus_8_1d.py index e34c154..0a49ea7 100644 --- a/Zeus_8_1d.py +++ b/Zeus_8_1d.py @@ -99,7 +99,7 @@ class Zeus_8_1d(IStrategy): }, "subplots": { "Rsi": { - "max_rsi_12": { + "max_rsi_24": { "color": "blue" }, }, @@ -145,6 +145,7 @@ class Zeus_8_1d(IStrategy): pair: { "first_buy": 0, "last_buy": 0.0, + "first_amount": 0.0, "last_min": 999999999999999.5, "last_max": 0, "trade_info": {}, @@ -405,16 +406,19 @@ class Zeus_8_1d(IStrategy): # self.pairs[pair]['force_buy'] = (self.pairs[pair]['count_of_buys'] - self.pairs[pair]['has_gain'] > 5) # return 'Baisse_' + pair_name + '_' + str(count_of_buys) + '_' + str(self.pairs[pair]['has_gain']) + '_' + str(round(baisse, 2)) - if last_candle['mid_smooth_5_deriv1'] <= -0.1 and profit > expected_profit and last_candle['rsi'] > 65: - self.pairs[pair]['force_sell'] = False - self.pairs[pair]['force_buy'] = (self.pairs[pair]['count_of_buys'] - self.pairs[pair]['has_gain'] > 5) - return 'RSI_' + pair_name + '_' + str(count_of_buys) + '_' + str(self.pairs[pair]['has_gain']) + '_' + str(round(baisse, 2)) + if last_candle['mid_smooth_5_deriv1'] <= -0.12 and profit > expected_profit and last_candle['max_rsi_24'] > 75 \ + and last_candle['mid_smooth_5_deriv2'] <= -0.15 : + return str(count_of_buys) + '_' + 'RSI80_' + pair_name + '_' + str(self.pairs[pair]['has_gain']) + '_' + str(round(baisse, 2)) - if last_candle['mid_smooth_24_deriv1'] <= -0.1 \ - and profit > expected_profit: + # if last_candle['mid_smooth_5_deriv1'] <= -0.1 and profit > expected_profit and last_candle['max_rsi_24'] > 65: + # self.pairs[pair]['force_sell'] = False + # self.pairs[pair]['force_buy'] = (self.pairs[pair]['count_of_buys'] - self.pairs[pair]['has_gain'] > 5) + # return str(count_of_buys) + '_' + 'RSI65_' + pair_name + '_' + str(self.pairs[pair]['has_gain']) + '_' + str(round(baisse, 2)) + + if last_candle['mid_smooth_24_deriv1'] <= -0.1 and last_candle['mid_smooth_5_deriv1'] <= -0 and profit > expected_profit: self.pairs[pair]['force_sell'] = False - self.pairs[pair]['force_buy'] = (self.pairs[pair]['count_of_buys'] - self.pairs[pair]['has_gain'] > 5) - return 'Drv3_' + pair_name + '_' + str(count_of_buys) + '_' + str(self.pairs[pair]['has_gain']) + '_' + str(round(baisse, 2)) + # self.pairs[pair]['force_buy'] = (self.pairs[pair]['count_of_buys'] - self.pairs[pair]['has_gain'] > 5) + return str(count_of_buys) + '_' + 'Drv3_' + pair_name + '_' + str(self.pairs[pair]['has_gain']) + '_' + str(round(baisse, 2)) self.pairs[pair]['max_touch'] = max(last_candle['close'], self.pairs[pair]['max_touch']) @@ -591,8 +595,9 @@ class Zeus_8_1d(IStrategy): # print(metadata['pair']) dataframe['rsi'] = talib.RSI(dataframe['close'], timeperiod=14) - dataframe['max_rsi_12'] = talib.MAX(dataframe['rsi'], timeperiod=12) - self.calculeDerivees(dataframe, 'rsi', horizon=12) + dataframe['max_rsi_24'] = talib.MAX(dataframe['rsi'], timeperiod=24) + dataframe['min_rsi_24'] = talib.MIN(dataframe['rsi'], timeperiod=24) + self.calculeDerivees(dataframe, 'rsi', horizon=24) dataframe['max12'] = talib.MAX(dataframe['close'], timeperiod=12) dataframe['min12'] = talib.MIN(dataframe['close'], timeperiod=12) @@ -663,6 +668,7 @@ class Zeus_8_1d(IStrategy): if count == 0: dataframe['first_price'] = buy.price self.pairs[pair]['first_buy'] = buy.price + self.pairs[pair]['first_amount'] = buy.price * buy.filled # dataframe['close01'] = buy.price * 1.01 # Order(id=2396, trade=1019, order_id=29870026652, side=buy, filled=0.00078, price=63921.01, @@ -674,12 +680,10 @@ class Zeus_8_1d(IStrategy): # dataframe['mid_price'] = (dataframe['last_price'] + dataframe['first_price']) / 2 count_buys = count # dataframe['limit'] = dataframe['last_price'] * (1 - self.baisse[count] / 100) - # dataframe['amount'] = amount self.pairs[pair]['total_amount'] = amount # Compter les baisses / hausses consécutives - self.calculateDownAndUp(dataframe, limit=0.0001) - + # self.calculateDownAndUp(dataframe, limit=0.0001) horizon_h = 12 dataframe['close_smooth'] = self.conditional_smoothing(dataframe['mid'].rolling(3).mean().dropna(), @@ -701,6 +705,32 @@ class Zeus_8_1d(IStrategy): self.calculeDerivees(dataframe, 'ema_volume', factor_1=10, factor_2=1, horizon=14) + dataframe['futur_percent_3'] = 100 * ((dataframe['sma5'].shift(-3) - dataframe['sma5']) / dataframe['sma5']) + futur_cols = ['futur_percent_3'] + indic_1 = 'mid_smooth_5_deriv1' + indic_2 = 'mid_smooth_5_deriv2' + + self.calculateProbabilite2Index(dataframe, futur_cols, indic_1, indic_2) + + ################### INFORMATIVE 1d + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe="1d") + heikinashi = qtpylib.heikinashi(informative) + informative['haopen'] = heikinashi['open'] + informative['haclose'] = heikinashi['close'] + informative['hapercent'] = (informative['haclose'] - informative['haopen']) / informative['haclose'] + informative = self.calculateDerivation(informative, window=5, suffixe="_5") + informative['sma5'] = talib.SMA(informative, timeperiod=5) + informative['sma20'] = talib.SMA(informative, timeperiod=20) + informative['max60'] = talib.MAX(informative['close'], timeperiod=60) + informative['min60'] = talib.MIN(informative['close'], timeperiod=60) + + self.calculeDerivees(informative, 'sma5', factor_1=10, factor_2=1, horizon=5) + self.calculeDerivees(informative, 'sma20', factor_1=10, factor_2=1, horizon=20) + if self.dp.runmode.value in ('backtest'): + informative['futur_percent_3'] = 100 * (informative['close'].shift(-3) - informative['close']) / informative['close'] + self.calculateProbabilite2Index(informative, futur_cols, indic_1, indic_2) + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, "1d", ffill=True) + return dataframe def calculeDerivees(self, dataframe, indic, factor_1=100, factor_2=10, horizon=5): @@ -801,13 +831,20 @@ class Zeus_8_1d(IStrategy): # (dataframe['mid_smooth_24_deriv1'].shift(1) <= 0) (dataframe['mid_smooth_24_deriv1'] >= 0.05) & (dataframe['mid_smooth_24_deriv2'] > 0) + & (dataframe['mid_smooth_5_deriv1'] > 0.001) & (dataframe['mid_smooth_5_deriv2'] > 0) # & (dataframe['hapercent'] > 0) - #& (dataframe['max_rsi_12'] < 50) + #& (dataframe['max_rsi_24'] < 50) # & (dataframe['open'] <= dataframe['bb_middleband']) ), ['enter_long', 'enter_tag']] = (1, 'smth_12') + dataframe.loc[ + ( + (dataframe['min_rsi_24'] < 20) + & (dataframe['hapercent'] > 0) + ), ['enter_long', 'enter_tag']] = (1, 'min_rsi_24') + dataframe['test'] = np.where(dataframe['enter_long'] == 1, dataframe['close'] * 1.01, np.nan) return dataframe @@ -990,12 +1027,12 @@ class Zeus_8_1d(IStrategy): return stake_amount return None except Exception as exception: - # print(exception) + print(exception) return None last_lost = self.getLastLost(last_candle, pair) - if (False and hours > 6 and last_candle['mid_smooth_5_deriv1'] > 0 and last_candle['mid_smooth_5_deriv2'] > 0): + if (hours > 6 and last_candle['mid_smooth_24_deriv1'] > 0.03 and last_candle['mid_smooth_5_deriv2'] > 0): try: stake_amount = self.pairs[pair]['first_amount'] / 4 if self.wallets.get_available_stake_amount() > stake_amount: @@ -1074,6 +1111,7 @@ class Zeus_8_1d(IStrategy): # pct60 = round(100 * self.getPctClose60D(pair, last_candle), 2) if True: # not pair in ('BTC/USDT', 'BTC/USDC'): + # factors = [1, 1.2, 1.3, 1.4] # factors = [1, 1.2, 1.3, 1.4] if self.pairs[pair]['count_of_buys'] == 0: # pctClose60 = self.getPctClose60D(pair, last_candle) @@ -1230,6 +1268,13 @@ class Zeus_8_1d(IStrategy): return self.labels[i] return self.labels[-1] # cas limite pour la borne max + def informative_pairs(self): + # get access to all pairs available in whitelist. + pairs = self.dp.current_whitelist() + informative_pairs = [(pair, '1d') for pair in pairs] + + return informative_pairs + def approx_val_from_bins(self, matrice, numeric_matrice, row_label, col_label): """ Renvoie une approximation de la valeur à partir des labels binaires (e.g. B5, H1) @@ -1401,7 +1446,6 @@ class Zeus_8_1d(IStrategy): # print(pivot_mean.round(2)) def should_enter_trade(self, pair: str, last_candle, current_time) -> bool: - return True limit = 3 @@ -1413,7 +1457,8 @@ class Zeus_8_1d(IStrategy): # if not pair.startswith('BTC'): dispo = round(self.wallets.get_available_stake_amount()) - if self.pairs[pair]['stop'] and last_candle['mid_smooth_5_deriv1'] > -0.9 and last_candle['sma5_deriv1'] > 0 and last_candle['sma5_deriv2'] > 0: + if self.pairs[pair]['stop'] and last_candle['mid_smooth_5_deriv1_1d'] > -0.9 \ + and last_candle['sma5_deriv1_1d'] > 0 and last_candle['sma5_deriv2_1d'] > 0: self.pairs[pair]['stop'] = False self.log_trade( last_candle=last_candle, @@ -1428,7 +1473,7 @@ class Zeus_8_1d(IStrategy): stake=0 ) else: - if self.pairs[pair]['stop'] == False and (last_candle['sma5_deriv1'] < -0.2 or last_candle['sma5_deriv2'] < -3): + if self.pairs[pair]['stop'] == False and (last_candle['sma5_deriv1_1d'] < -0.2 or last_candle['sma5_deriv2_1d'] < -3): self.pairs[pair]['stop'] = True # if self.pairs[pair]['current_profit'] > 0: # self.pairs[pair]['force_sell'] = True