266 lines
9.6 KiB
Python
266 lines
9.6 KiB
Python
# 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
|