diff --git a/Frictrade.py b/Frictrade.py new file mode 100644 index 0000000..e8f0fbd --- /dev/null +++ b/Frictrade.py @@ -0,0 +1,773 @@ +# 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 +import os +import json +import csv +from pandas import DataFrame +from typing import Optional, Union, Tuple +import math +import logging +from pathlib import Path + +# -------------------------------- + +# Add your lib to import here test git +import ta +import talib.abstract as talib +import freqtrade.vendor.qtpylib.indicators as qtpylib +from datetime import timezone, timedelta + +logger = logging.getLogger(__name__) + +# 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" + + +class Frictrade(IStrategy): + startup_candle_count = 60 * 24 + + # ROI table: + minimal_roi = { + "0": 0.564, + "567": 0.273, + "2814": 0.12, + "7675": 0 + } + + # Stoploss: + stoploss = -1 # 0.256 + # Custom stoploss + use_custom_stoploss = False + + trailing_stop = True + trailing_stop_positive = 0.15 + trailing_stop_positive_offset = 0.1 + trailing_only_offset_is_reached = False + + # Buy hypers + timeframe = '1m' + max_open_trades = 5 + max_amount = 40 + + parameters = {} + # DCA config + position_adjustment_enable = True + + columns_logged = False + pairs = { + pair: { + "first_buy": 0, + "last_buy": 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, + 'previous_profit': 0, + "last_candle": {}, + "last_count_of_buys": 0, + 'base_stake_amount': 0, + 'stop_buy': False, + 'last_date': 0, + 'stop': False, + 'max_profit': 0, + 'first_amount': 0, + '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"] + } + trades = list() + max_profit_pairs = {} + + 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() + + condition = True #(last_candle[f"{indic_5m}_deriv1"] >= indic_deriv1_5m) and (last_candle[f"{indic_5m}_deriv2"] >= indic_deriv2_5m) + + allow_to_buy = True #(condition and not self.pairs[pair]['stop']) | (entry_tag == 'force_entry') + + 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_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 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() + + minutes = int(round((current_time - trade.open_date_utc).seconds / 60, 0)) + profit =trade.calc_profit(rate) + force = self.pairs[pair]['force_sell'] + allow_to_sell = minutes > 30 and (last_candle['hapercent'] < 0 ) or force or (exit_reason == 'force_exit') or (exit_reason == 'stop_loss') + + 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_candle'] = last_candle + self.pairs[pair]['max_profit'] = 0 + self.pairs[pair]['previous_profit'] = 0 + 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(profit, 2) + ) + 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]['current_trade'] = None + # else: + # self.printLog(f"{current_time} SELL triggered for {pair} ({exit_reason} profit={profit} minutes={minutes} percent={last_candle['hapercent']}) but condition blocked") + return (allow_to_sell) | (exit_reason == 'force_exit') | (exit_reason == 'stop_loss') + + # def custom_exit(self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs): + # + # dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + # last_candle = dataframe.iloc[-1].squeeze() + # last_candle_1h = dataframe.iloc[-13].squeeze() + # before_last_candle = dataframe.iloc[-2].squeeze() + # before_last_candle_2 = dataframe.iloc[-3].squeeze() + # before_last_candle_12 = dataframe.iloc[-13].squeeze() + # + # expected_profit = self.expectedProfit(pair, last_candle) + # # print(f"current_time={current_time} current_profit={current_profit} expected_profit={expected_profit}") + # + # max_touch_before = self.pairs[pair]['max_touch'] + # 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 = trade.calc_profit(current_rate) #round(current_profit * trade.stake_amount, 1) + # self.pairs[pair]['max_profit'] = max(self.pairs[pair]['max_profit'], profit) + # max_profit = last_candle['max5'] #self.pairs[pair]['max_profit'] + # baisse = 0 + # if profit > 0: + # baisse = 1 - (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 + # minutes = (current_time - trade.date_last_filled_utc).total_seconds() / 60.0 + # + # if minutes % 4 == 0: + # self.log_trade( + # last_candle=last_candle, + # date=current_time, + # action="🟢 CURRENT", #🔴 CURRENT" if self.pairs[pair]['stop'] or last_candle['stop_buying'] else " + # dispo=dispo, + # pair=pair, + # rate=last_candle['close'], + # trade_type='', + # profit=round(profit, 2), + # buys=count_of_buys, + # stake=0 + # ) + # + # if (last_candle['close'] > last_candle['mid']) or (last_candle['sma5_deriv1'] > 0): + # return None + # + # pair_name = self.getShortName(pair) + # + # if profit > 0.003 * count_of_buys and baisse > 0.30: + # self.pairs[pair]['force_sell'] = False + # self.pairs[pair]['force_buy'] = (self.pairs[pair]['count_of_buys'] - self.pairs[pair]['has_gain'] > 3) + # return str(count_of_buys) + '_' + 'B30_' + pair_name + '_' + str(self.pairs[pair]['has_gain']) + # + # self.pairs[pair]['max_touch'] = max(last_candle['close'], self.pairs[pair]['max_touch']) + + def getShortName(self, pair): + return pair.replace("/USDT", '').replace("/USDC", '').replace("_USDC", '').replace("_USDT", '') + + def getLastLost(self, last_candle, pair): + last_lost = round((last_candle['close'] - self.pairs[pair]['max_touch']) / self.pairs[pair]['max_touch'], 3) + return last_lost + 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 expectedProfit(self, pair: str, last_candle: DataFrame): + lim = 0.01 + pct = 0.002 + if (self.getShortName(pair) == 'BTC'): + lim = 0.005 + pct = 0.001 + pct_to_max = lim + pct * self.pairs[pair]['count_of_buys'] + expected_profit = lim * self.pairs[pair]['total_amount'] # min(3 * lim, max(lim, pct_to_max)) # 0.004 + 0.002 * self.pairs[pair]['count_of_buys'] #min(0.01, first_max) + + self.pairs[pair]['expected_profit'] = expected_profit + + return expected_profit + + def log_trade(self, action, pair, date, trade_type=None, rate=None, dispo=None, profit=None, buys=None, stake=None, + last_candle=None): + # 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} " + f"| {'Pct':>6} | {'max_touch':>11} | {'last_lost':>12} | {'last_max':>7}| {'last_max':>7}|{'Buys':>5}| {'Stake':>5} |" + f"{'rsi':>6}" #|Distmax|s201d|s5_1d|s5_2d|s51h|s52h|smt1h|smt2h|tdc1d|tdc1h" + ) + self.printLineLog() + df = pd.DataFrame.from_dict(self.pairs, orient='index') + colonnes_a_exclure = ['last_candle', + 'trade_info', 'last_date', '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'] + + self.printLog(df_filtered) + + self.columns_logged += 1 + date = str(date)[:16] if date else "-" + limit = None + 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 = '' + 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 = '' + + 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) + + color = GREEN if profit > 0 else RED + + 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. + 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"{round(last_candle['max_rsi_24'], 1) or '-' :>6}|" + ) + + def printLineLog(self): + # f"sum1h|sum1d|Tdc|Tdh|Tdd| drv1 |drv|drv_1d|" + self.printLog( + f"+{'-' * 18}+{'-' * 12}+{'-' * 5}+{'-' * 20}+{'-' * 9}+{'-' * 8}+{'-' * 12}+{'-' * 8}+{'-' * 13}+{'-' * 14}+{'-' * 9}{'-' * 9}+{'-' * 5}+{'-' * 7}+" + 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 populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Add all ta features + pair = metadata['pair'] + short_pair = self.getShortName(pair) + self.path = f"user_data/plots/{short_pair}/" + ("valide/" if not self.dp.runmode.value in ('backtest') else '') + + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['haopen'] = heikinashi['open'] + dataframe['haclose'] = heikinashi['close'] + dataframe['hapercent'] = (dataframe['haclose'] - dataframe['haopen']) / dataframe['haclose'] + + dataframe['mid'] = dataframe['open'] + (dataframe['close'] - dataframe['open']) / 2 + dataframe['sma5'] = dataframe['mid'].ewm(span=5, adjust=False).mean() #dataframe["mid"].rolling(window=5).mean() + dataframe['sma5_deriv1'] = 1000 * (dataframe['sma5'] - dataframe['sma5'].shift(1)) / dataframe['sma5'].shift(1) + dataframe['sma60'] = dataframe['mid'].ewm(span=60, adjust=False).mean() + dataframe['sma60_deriv1'] = 1000 * (dataframe['sma60'] - dataframe['sma60'].shift(1)) / dataframe['sma60'].shift(1) + + # dataframe[f"sma5_inv"] = (dataframe[f"sma5"].shift(2) >= dataframe[f"sma5"].shift(1)) \ + # & (dataframe[f"sma5"].shift(1) <= dataframe[f"sma5"]) + + dataframe["sma5_sqrt"] = ( + np.sqrt(np.abs(dataframe["sma5"] - dataframe["sma5"].shift(1))) + + np.sqrt(np.abs(dataframe["sma5"].shift(2) - dataframe["sma5"].shift(1))) + ) + dataframe["sma5_inv"] = ( + (dataframe["sma5"].shift(2) >= dataframe["sma5"].shift(1)) + & (dataframe["sma5"].shift(1) <= dataframe["sma5"]) + & (dataframe["sma5_sqrt"] > 5) + ) + + dataframe["percent"] = dataframe['mid'].pct_change() + dataframe["percent3"] = dataframe['mid'].pct_change(3).rolling(3).mean() + dataframe["percent12"] = dataframe['mid'].pct_change(12).rolling(12).mean() + dataframe["percent24"] = dataframe['mid'].pct_change(24).rolling(24).mean() + + dataframe['rsi'] = talib.RSI(dataframe['mid'], timeperiod=14) + dataframe['max_rsi_12'] = talib.MAX(dataframe['rsi'], timeperiod=12) + dataframe['max_rsi_24'] = talib.MAX(dataframe['rsi'], timeperiod=24) + dataframe['max5'] = talib.MAX(dataframe['mid'], timeperiod=5) + dataframe['min180'] = talib.MIN(dataframe['mid'], timeperiod=180) + dataframe['max180'] = talib.MAX(dataframe['mid'], timeperiod=180) + + # # ################### INFORMATIVE 1h + # informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1h') + # informative = self.populate1hIndicators(df=informative, metadata=metadata) + # # informative = self.calculateRegression(informative, 'mid', lookback=15) + # dataframe = merge_informative_pair(dataframe, informative, '1m', '1h', ffill=True) + + dataframe['last_price'] = dataframe['close'] + dataframe['first_price'] = dataframe['close'] + if self.dp: + if self.dp.runmode.value in ('live', 'dry_run'): + self.getOpenTrades() + + for trade in self.trades: + if trade.pair != pair: + continue + filled_buys = trade.select_filled_orders('buy') + count = 0 + amount = 0 + for buy in filled_buys: + 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, + # status=closed, date=2024-08-26 02:20:11) + dataframe['last_price'] = buy.price + self.pairs[pair]['last_buy'] = buy.price + count = count + 1 + amount += buy.price * buy.filled + count_buys = count + self.pairs[pair]['total_amount'] = amount + + return dataframe + + def getOpenTrades(self): + # if len(self.trades) == 0: + self.trades = Trade.get_open_trades() + return self.trades + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe['sma5_inv'] == 1) + # & ( + # (dataframe['percent3'] <= -0.003) + # | (dataframe['percent12'] <= -0.003) + # | (dataframe['percent24'] <= -0.003) + # ) + ), ['enter_long', 'enter_tag']] = (1, f"future") + + dataframe['test'] = np.where(dataframe['enter_long'] == 1, dataframe['close'] * 1.003, np.nan) + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + return dataframe + + def adjust_stake_amount(self, pair: str, last_candle: DataFrame): + # Calculer le minimum des 14 derniers jours + nb_pairs = len(self.dp.current_whitelist()) + + base_stake_amount = self.config.get('stake_amount') + + if True : #self.pairs[pair]['count_of_buys'] == 0: + factor = 1 #65 / min(65, last_candle['rsi_1d']) + # if last_candle['min_max_60'] > 0.04: + # factor = 2 + + adjusted_stake_amount = base_stake_amount #max(base_stake_amount / 5, base_stake_amount * factor) + else: + adjusted_stake_amount = self.pairs[pair]['first_amount'] + + if self.pairs[pair]['count_of_buys'] == 0: + self.pairs[pair]['first_amount'] = adjusted_stake_amount + + return adjusted_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): + # ne rien faire si ordre deja en cours + if trade.has_open_orders: + # self.printLog("skip open orders") + return None + if (self.wallets.get_available_stake_amount() < 10): # or trade.stake_amount >= max_stake: + return 0 + + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + before_last_candle = dataframe.iloc[-2].squeeze() + # prépare les données + current_time = current_time.astimezone(timezone.utc) + open_date = trade.open_date.astimezone(timezone.utc) + 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 + count_of_buys = trade.nr_of_successful_entries + current_time_utc = current_time.astimezone(timezone.utc) + open_date = trade.open_date.astimezone(timezone.utc) + days_since_open = (current_time_utc - open_date).days + pair = trade.pair + profit = trade.calc_profit(current_rate) #round(current_profit * trade.stake_amount, 1) + last_lost = self.getLastLost(last_candle, pair) + pct_first = 0 + + total_counts = sum( + pair_data['count_of_buys'] for pair_data in self.pairs.values() if not self.getShortName(pair) == 'BTC') + + if self.pairs[pair]['first_buy']: + pct_first = self.getPctFirstBuy(pair, last_candle) + + lim = 0.3 + if (len(dataframe) < 1): + # self.printLog("skip dataframe") + return None + + # Dernier prix d'achat réel (pas le prix moyen) + last_fill_price = self.pairs[trade.pair]['last_buy'] #trade.open_rate # remplacé juste après ↓ + + # if len(trade.orders) > 0: + # # On cherche le dernier BUY exécuté + # buy_orders = [o for o in trade.orders if o.is_buy and o.status == "closed"] + # if buy_orders: + # last_fill_price = buy_orders[-1].price + + # baisse relative + dca_threshold = 0.0025 * count_of_buys + decline = (last_fill_price - current_rate) / last_fill_price + increase = - decline + + # if decline >= self.dca_threshold: + # # Exemple : on achète 50% du montant du dernier trade + # last_amount = buy_orders[-1].amount if buy_orders else 0 + # stake_amount = last_amount * current_rate * 0.5 + # return stake_amount + + + condition = last_candle['hapercent'] > 0 + limit_buy = 40 + if decline >= dca_threshold: + try: + if self.pairs[pair]['has_gain'] and profit > 0: + self.pairs[pair]['force_sell'] = True + self.pairs[pair]['previous_profit'] = profit + return None + + max_amount = self.config.get('stake_amount') * 2.5 + stake_amount = min(min(max_amount, self.wallets.get_available_stake_amount()), + self.adjust_stake_amount(pair, last_candle)) + # print(f"profit={profit} previous={self.pairs[pair]['previous_profit']} count_of_buys={trade.nr_of_successful_entries}") + if stake_amount > 0: + self.pairs[pair]['previous_profit'] = profit + trade_type = "Loss " + (last_candle['enter_tag'] if last_candle['enter_long'] == 1 else '') + self.pairs[trade.pair]['count_of_buys'] += 1 + self.pairs[pair]['total_amount'] += stake_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(profit, 1), + buys=trade.nr_of_successful_entries + 1, + stake=round(stake_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 + + # df = pd.DataFrame.from_dict(self.pairs, orient='index') + # colonnes_a_exclure = ['last_candle', 'stop', + # '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'] + # + # self.printLog(df_filtered) + + return stake_amount + return None + except Exception as exception: + self.printLog(exception) + return None + + if (increase >= dca_threshold and self.wallets.get_available_stake_amount() > 0): + try: + self.pairs[pair]['previous_profit'] = profit + stake_amount = min(self.wallets.get_available_stake_amount(), self.adjust_stake_amount(pair, last_candle)) + if stake_amount > 0: + self.pairs[pair]['has_gain'] += 1 + + trade_type = 'Gain +' + (last_candle['enter_tag'] if last_candle['enter_long'] == 1 else '') + self.pairs[trade.pair]['count_of_buys'] += 1 + self.pairs[pair]['total_amount'] += stake_amount + self.log_trade( + last_candle=last_candle, + date=current_time, + action="🟡 Gain +", + dispo=dispo, + pair=trade.pair, + rate=current_rate, + trade_type='Gain', + profit=round(profit, 1), + buys=trade.nr_of_successful_entries + 1, + stake=round(stake_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 + return stake_amount + return None + except Exception as exception: + self.printLog(exception) + return None + + return None + + def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs): + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + last_candle_1h = dataframe.iloc[-13].squeeze() + before_last_candle = dataframe.iloc[-2].squeeze() + before_last_candle_2 = dataframe.iloc[-3].squeeze() + before_last_candle_12 = dataframe.iloc[-13].squeeze() + + expected_profit = self.expectedProfit(pair, last_candle) + # print(f"current_time={current_time} current_profit={current_profit} expected_profit={expected_profit}") + + # ----- 1) Charger les variables de trailing pour ce trade ----- + max_price = self.pairs[pair]['max_touch'] + + 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 = trade.calc_profit(current_rate) #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 = 1 - (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 + minutes = (current_time - trade.date_last_filled_utc).total_seconds() / 60.0 + + # ----- 2) Mise à jour du max_price ----- + self.pairs[pair]['max_touch'] = max(last_candle['close'], self.pairs[pair]['max_touch']) + + # ----- 3) Calcul du profit max atteint ----- + profit_max = (max_price - trade.open_rate) / trade.open_rate + + # ----- 5) Calcul du trailing stop dynamique ----- + # Exemple : offset=0.321 => stop à +24.8% + trailing_stop = max_price * (1 - self.trailing_stop_positive) + + if minutes % 15 == 0: + self.log_trade( + last_candle=last_candle, + date=current_time, + action="🟢 CURRENT", #🔴 CURRENT" if self.pairs[pair]['stop'] or last_candle['stop_buying'] else " + dispo=dispo, + pair=pair, + rate=last_candle['close'], + trade_type=f"{round(profit_max * 100, 2)} - {round(trailing_stop,0)}", + profit=round(profit, 2), + buys=count_of_buys, + stake=0 + ) + + # ----- 4) OFFSET : faut-il attendre de dépasser trailing_stop_positive_offset ? ----- + if self.trailing_only_offset_is_reached: + if profit_max < self.trailing_stop_positive_offset: + return None # ne pas activer le trailing encore + # Sinon : trailing actif dès le début + + # ----- 6) Condition de vente ----- + if current_rate <= trailing_stop and self.wallets.get_available_stake_amount() < 300: + return f"stop_{count_of_buys}" + return None + + def informative_pairs(self): + # get access to all pairs available in whitelist. + pairs = self.dp.current_whitelist() + informative_pairs = [(pair, '1h') for pair in pairs] + # informative_pairs += [(pair, '1d') for pair in pairs] + + return informative_pairs + + def populate1hIndicators(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: + + # --- WEEKLY LEVELS --- + # semaine précédente = semaine ISO différente + df["week"] = df.index.isocalendar().week + df["year"] = df.index.year + + df["weekly_low"] = ( + df.groupby(["year", "week"])["low"] + .transform("min") + .shift(1) # décalé -> pas regarder la semaine en cours + ) + df["weekly_high"] = ( + df.groupby(["year", "week"])["high"] + .transform("max") + .shift(1) + ) + + # Définition simple d'une zone de demande hebdo : + # bas + 25% de la bougie => modifiable + df["weekly_demand_zone_low"] = df["weekly_low"] + df["weekly_demand_zone_high"] = df["weekly_low"] * 1.025 + + # --- MONTHLY LEVELS --- + df["month"] = df.index.month + + df["monthly_low"] = ( + df.groupby(["year", "month"])["low"] + .transform("min") + .shift(1) # mois précédent uniquement + ) + df["monthly_high"] = ( + df.groupby(["year", "month"])["high"] + .transform("max") + .shift(1) + ) + + df["monthly_demand_zone_low"] = df["monthly_low"] + df["monthly_demand_zone_high"] = df["monthly_low"] * 1.03 + + return df + + # ----- SIGNALS SIMPLES POUR EXEMPLE ----- + + # def populate_buy_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: + # df["buy"] = 0 + # + # # Exemple : acheter si le prix tape la zone de demande hebdomadaire + # df.loc[ + # (df["close"] <= df["weekly_demand_zone_high"]) & + # (df["close"] >= df["weekly_demand_zone_low"]), + # "buy" + # ] = 1 + # + # return df + # + # def populate_sell_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: + # df["sell"] = 0 + # + # # Exemple : vendre sur retour au weekly_high précédent + # df.loc[df["close"] >= df["weekly_high"], "sell"] = 1 + # + # return df + diff --git a/tools/trailing_stop_loss.png b/tools/trailing_stop_loss.png new file mode 100644 index 0000000..9b7b128 Binary files /dev/null and b/tools/trailing_stop_loss.png differ