TEST Simple

This commit is contained in:
Jérôme Delacotte
2025-10-20 20:54:24 +02:00
parent ee45dc890d
commit 4c0692426e
3 changed files with 963 additions and 20 deletions

265
Simple.py Normal file
View File

@@ -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

633
Simple_01.py Normal file
View File

@@ -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 saccé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 saccé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']}"
)

View File

@@ -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