OSIRIS ELLIOTT WAVE — The Wave Principle em código.
Timeframe
5m
Direction
Long Only
Stoploss
-5.0%
Trailing Stop
No
ROI
0m: 10.0%, 30m: 5.0%, 60m: 3.0%, 120m: 2.0%
Interface Version
3
Startup Candles
300
Indicators
6
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
OSIRIS ELLIOTT STRATEGY v1.0 — The Wave Principle
================================================================
Baseada em:
- R.N. Elliott, "The Wave Principle" (1938)
- R.N. Elliott, "Nature's Law: The Secret of the Universe" (1946)
- Frost & Prechter, "Elliott Wave Principle: Key to Market Behavior" (2005, 10th ed.)
- Steven W. Poser, "Applying Elliott Wave Theory Profitably" (2003)
TEORIA CORE (R.N. Elliott, 1871-1948):
Mercados se movem em dois tipos de ondas:
IMPULSIVAS (5 ondas): 1-2-3-4-5 (a favor da tendência)
CORRETIVAS (3 ondas): A-B-C (contra a tendência)
3 REGRAS INVIOLÁVEIS (Frost & Prechter):
R1: Onda 2 NUNCA retrace mais de 100% da Onda 1
R2: Onda 3 NUNCA é a mais curta das ondas impulsivas (1, 3 e 5)
R3: Onda 4 NUNCA entra no território de preço da Onda 1
PROPORÇÕES FIBONACCI (base da teoria — "The Fibonacci Summation Series is
the basis of The Wave Principle" — R.N. Elliott, Masterworks):
Retracements: 23.6%, 38.2%, 50%, 61.8%, 78.6%
Extensions: 100%, 127.2%, 161.8%, 200%, 261.8%
ONDAS E SUAS PERSONALIDADES (Frost & Prechter, p.31, 78-85):
Wave 1: raramente óbvia no início; notícias negativas; volume sobe levemente
Wave 2: corrije Wave1, mas nunca ultrapassa início; sentimento bearish;
retrace típico 50%-61.8% de W1; padrão de 3 ondas
Wave 3: MAIS PODEROSA — notícias positivas; preço sobe rápido;
pullbacks curtos; frequentemente 1.618x o tamanho de Wave1
Wave 4: claramente corretiva; lateral; retrace < 38.2% de Wave3;
volume bem abaixo de Wave3; NÃO entra no território de Wave1
Wave 5: última perna bullish; todos bullish; divergências de momentum;
frequentemente = Wave1 em tamanho (igualdade)
ESTRATÉGIA DE ENTRADA:
SETUP A — "Wave 3 Onset" (principal):
1. Detectar Wave 1 (pivô mínimo → pivô máximo)
2. Aguardar Wave 2 (retrace 38.2%-61.8% de Wave1)
3. Validar R1: Wave2_low > Wave1_start
4. Confirmar reversão de Wave2 com RSI + MACD
5. Entry: breakout acima do topo de Wave1
6. Stop: abaixo do fundo de Wave2
7. Target: Wave2_low + Wave1_length * 1.618
SETUP B — "Wave 5 Entry" (secundário):
1. Detectar estrutura W1-W2-W3-W4 completa
2. Validar R3: Wave4_low > Wave1_high
3. Wave3 > Wave1 em tamanho (R2 parcial)
4. Entry: reversão de Wave4 com RSI divergência
5. Target: Wave1_length projetado do fundo de Wave4
PADRÕES CORRETIVOS (quando NÃO entrar):
Zigzag (5-3-5): correção abrupta — comum em Wave2
Flat (3-3-5): correção lateral — comum em Wave4
Triangle(3-3-3-3-3): contração de range — apenas Wave4 ou B
Target diário: 10 operações/dia | WR 80%+ | R:R 2:1 a 3:1
Timeframe: 5m | Spot BTC/ETH/BNB/SOL/XRP
"""
import logging
import numpy as np
from pandas import DataFrame
from typing import Optional
from freqtrade.strategy import IStrategy
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
from freqtrade.persistence import Trade
import talib.abstract as ta
try:
from freqtrade.strategy import stoploss_from_open
except ImportError:
def stoploss_from_open(open_relative_stop, current_profit, is_short=False):
if current_profit == 0:
return 1
if is_short:
return -1 + ((1 - open_relative_stop) / (1 - current_profit))
return 1 - ((1 + open_relative_stop) / (1 + current_profit))
logger = logging.getLogger(__name__)
class OsirisElliottStrategy(IStrategy):
"""
OSIRIS ELLIOTT WAVE — The Wave Principle em código.
Detecta padrões de Ondas de Elliott (5-waves impulsivas) e entra
no Setup Wave 3 (mais poderoso) e Setup Wave 5 (extensão).
REGRAS DO LIVRO. Stop/TP via proporções Fibonacci.
"""
INTERFACE_VERSION = 3
can_short = False
timeframe = "5m"
# ROI safety net (hyperopt vai afinar)
minimal_roi = {
"0": 0.10,
"30": 0.05,
"60": 0.03,
"120": 0.02,
}
stoploss = -0.05
trailing_stop = False
startup_candle_count = 300
process_only_new_candles = True
# ===================================================================
# HYPEROPT PARAMETERS
# ===================================================================
# --- Detecção de pivôs ---
pivot_period = IntParameter(5, 20, default=10, space="buy", optimize=True)
# Min % de swing para um pivô ser significativo
pivot_strength = DecimalParameter(0.003, 0.02, default=0.008, space="buy", optimize=True)
# --- Wave 2 Fibonacci (retracement de Wave1) ---
# Wave 2 tipicamente retrace 50-61.8% de Wave1 (Frost & Prechter p.78)
wave2_fib_min = DecimalParameter(0.30, 0.55, default=0.382, space="buy", optimize=True)
wave2_fib_max = DecimalParameter(0.60, 0.90, default=0.618, space="buy", optimize=True)
# --- Wave 4 Fibonacci (retracement de Wave3) ---
# Wave 4 tipicamente retrace 23.6-38.2% de Wave3 (Frost & Prechter p.83)
wave4_fib_min = DecimalParameter(0.15, 0.35, default=0.236, space="buy", optimize=True)
wave4_fib_max = DecimalParameter(0.30, 0.618, default=0.382, space="buy", optimize=True)
# --- Wave 3 extensão target (Frost & Prechter: "Wave 3 often extends 1.618:1") ---
wave3_ext = DecimalParameter(1.272, 2.618, default=1.618, space="buy", optimize=True)
# --- Score mínimo para entrar (0-10) ---
buy_score_min = IntParameter(4, 9, default=6, space="buy", optimize=True)
# --- Confirmação RSI ---
rsi_w2_max = IntParameter(25, 55, default=45, space="buy", optimize=True) # Wave2 bottom
rsi_w4_max = IntParameter(30, 60, default=50, space="buy", optimize=True) # Wave4 bottom
# --- Setup preference ---
use_wave3_setup = CategoricalParameter([True, False], default=True, space="buy", optimize=False)
use_wave5_setup = CategoricalParameter([True, False], default=True, space="buy", optimize=False)
# --- Stop/TP multipliers ---
stop_buffer_atr = DecimalParameter(0.3, 1.5, default=0.5, space="stoploss", optimize=True)
tp_rr_ratio = DecimalParameter(1.5, 4.0, default=2.0, space="sell", optimize=True)
# ===================================================================
# INDICADORES
# ===================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# --- Base TA ---
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
dataframe['rsi_fast'] = ta.RSI(dataframe, timeperiod=7)
dataframe['ema20'] = ta.EMA(dataframe, timeperiod=20)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
# MACD para confirmação de reversão de Wave2/Wave4
macd, macdsignal, macdhist = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd
dataframe['macd_signal'] = macdsignal
dataframe['macd_hist'] = macdhist
# Volume ratio (Wave1 volume deve ser > Wave2 volume — Frost & Prechter)
dataframe['volume_ma'] = dataframe['volume'].rolling(20).mean()
dataframe['volume_ratio'] = dataframe['volume'] / (dataframe['volume_ma'] + 1e-9)
# --- Pivôs Williams Fractal (5-bar, 2-bar lag) ---
# Um fractal high em barra i é confirmado na barra i+2
# high[i] > high[i-1], high[i-2], high[i+1], high[i+2]
dataframe['fractal_high'] = np.where(
(dataframe['high'].shift(2) > dataframe['high'].shift(4)) &
(dataframe['high'].shift(2) > dataframe['high'].shift(3)) &
(dataframe['high'].shift(2) > dataframe['high'].shift(1)) &
(dataframe['high'].shift(2) > dataframe['high']),
dataframe['high'].shift(2),
np.nan
)
dataframe['fractal_low'] = np.where(
(dataframe['low'].shift(2) < dataframe['low'].shift(4)) &
(dataframe['low'].shift(2) < dataframe['low'].shift(3)) &
(dataframe['low'].shift(2) < dataframe['low'].shift(1)) &
(dataframe['low'].shift(2) < dataframe['low']),
dataframe['low'].shift(2),
np.nan
)
# Pivôs suavizados por período maior (para ondas maiores)
period = 10 # default; hyperopt vai tunar via pivot_period
for p in [10, 15, 20]:
col_h = f'pivot_high_{p}'
col_l = f'pivot_low_{p}'
roll_max = dataframe['high'].rolling(2 * p + 1, center=True, min_periods=1).max()
roll_min = dataframe['low'].rolling(2 * p + 1, center=True, min_periods=1).min()
dataframe[col_h] = np.where(dataframe['high'] == roll_max, dataframe['high'], np.nan)
dataframe[col_l] = np.where(dataframe['low'] == roll_min, dataframe['low'], np.nan)
# --- Elliott Wave Pattern Scoring ---
dataframe['ew_setup_a_score'] = 0.0 # Wave 3 setup
dataframe['ew_setup_b_score'] = 0.0 # Wave 5 setup
dataframe['ew_w1_start'] = np.nan
dataframe['ew_w1_end'] = np.nan
dataframe['ew_w2_end'] = np.nan
dataframe['ew_w3_end'] = np.nan
dataframe['ew_w4_end'] = np.nan
dataframe['ew_target_w3'] = np.nan
dataframe['ew_target_w5'] = np.nan
dataframe['ew_stop'] = np.nan
dataframe = self._compute_elliott_signals(dataframe)
return dataframe
# ===================================================================
# CÁLCULO DOS SINAIS ELLIOTT
# ===================================================================
def _compute_elliott_signals(self, df: DataFrame) -> DataFrame:
"""
Detecta padrões Elliott Wave usando pivôs confirmados.
Abordagem:
1. Extrai pivôs significativos (alternando high/low)
2. Para cada barra, olha os últimos 4-6 pivôs
3. Tenta encaixar padrão W0-W1-W2 (Setup A) ou W0-W1-W2-W3-W4 (Setup B)
4. Valida as 3 regras invioláveis + proporções Fibonacci
5. Pontua por confluência
"""
# Extrair lista de pivôs ordenados por tempo
# Usando pivot_high_10 e pivot_low_10 como base
pivot_highs = df['pivot_high_10'].dropna()
pivot_lows = df['pivot_low_10'].dropna()
# Combinar e ordenar por índice posicional
pivots = []
for idx in pivot_highs.index:
pos = df.index.get_loc(idx)
pivots.append((pos, 'H', float(pivot_highs[idx])))
for idx in pivot_lows.index:
pos = df.index.get_loc(idx)
pivots.append((pos, 'L', float(pivot_lows[idx])))
pivots.sort(key=lambda x: x[0])
# Remover pivôs consecutivos do mesmo tipo (manter apenas o extremo)
cleaned = []
for pv in pivots:
if cleaned and cleaned[-1][1] == pv[1]:
# Mesmo tipo: manter o mais extremo
if pv[1] == 'H' and pv[2] > cleaned[-1][2]:
cleaned[-1] = pv
elif pv[1] == 'L' and pv[2] < cleaned[-1][2]:
cleaned[-1] = pv
else:
cleaned.append(pv)
if len(cleaned) < 4:
return df
# Para cada barra, analisa o contexto dos pivôs recentes
n = len(df)
# Arrays para saída
setup_a_score = np.zeros(n)
setup_b_score = np.zeros(n)
w1_start_arr = np.full(n, np.nan)
w1_end_arr = np.full(n, np.nan)
w2_end_arr = np.full(n, np.nan)
w3_end_arr = np.full(n, np.nan)
w4_end_arr = np.full(n, np.nan)
target_w3_arr = np.full(n, np.nan)
target_w5_arr = np.full(n, np.nan)
stop_arr = np.full(n, np.nan)
# Iterar sobre barras (processa cada barra com base nos pivôs até essa barra)
for bar_idx in range(50, n):
# Pivôs disponíveis até esta barra (com 2 barras de lag de fractal)
avail = [p for p in cleaned if p[0] <= bar_idx - 2]
if len(avail) < 4:
continue
current_price = float(df['close'].iloc[bar_idx])
current_rsi = float(df['rsi'].iloc[bar_idx]) if not np.isnan(df['rsi'].iloc[bar_idx]) else 50.0
current_macd_hist = float(df['macd_hist'].iloc[bar_idx]) if not np.isnan(df['macd_hist'].iloc[bar_idx]) else 0.0
current_atr = float(df['atr'].iloc[bar_idx]) if not np.isnan(df['atr'].iloc[bar_idx]) else 0.001
# ---- SETUP A: Wave 3 Onset ----
# Precisa dos últimos 3 pivôs: W0(L) → W1(H) → W2(L)
# Procura o padrão: Low, High, Low nos últimos pivôs
score_a, w0, w1h, w2l, tp_w3, sl = self._check_wave3_setup(
avail, current_price, current_rsi, current_macd_hist, current_atr
)
if score_a > 0:
setup_a_score[bar_idx] = score_a
if w0 is not None:
w1_start_arr[bar_idx] = w0[2]
w1_end_arr[bar_idx] = w1h[2]
w2_end_arr[bar_idx] = w2l[2]
target_w3_arr[bar_idx] = tp_w3
stop_arr[bar_idx] = sl
# ---- SETUP B: Wave 5 Entry ----
# Precisa dos últimos 5 pivôs: W0(L) → W1(H) → W2(L) → W3(H) → W4(L)
score_b, w3h, w4l, tp_w5, sl5 = self._check_wave5_setup(
avail, current_price, current_rsi, current_macd_hist, current_atr
)
if score_b > 0:
setup_b_score[bar_idx] = score_b
if w4l is not None:
w4_end_arr[bar_idx] = w4l[2]
target_w5_arr[bar_idx] = tp_w5
if stop_arr[bar_idx] == 0:
stop_arr[bar_idx] = sl5
df['ew_setup_a_score'] = setup_a_score
df['ew_setup_b_score'] = setup_b_score
df['ew_w1_start'] = w1_start_arr
df['ew_w1_end'] = w1_end_arr
df['ew_w2_end'] = w2_end_arr
df['ew_w3_end'] = w3_end_arr
df['ew_w4_end'] = w4_end_arr
df['ew_target_w3'] = target_w3_arr
df['ew_target_w5'] = target_w5_arr
df['ew_stop'] = stop_arr
return df
def _check_wave3_setup(self, pivots, price, rsi, macd_hist, atr):
"""
Valida Setup A — Início da Onda 3.
Procura o padrão: W0(Low) → W1(High) → W2(Low)
e checa se o preço atual está quebrando acima do topo de W1.
Retorna: (score, w0_pivot, w1_pivot, w2_pivot, tp, sl)
SCORE componentes (máx ~10):
R1 validation: +2 (Wave2 > Wave1 start — REGRA INVIOLÁVEL)
Fib W2 em zona ideal: +2 (38.2%-61.8% de W1 — Frost & Prechter p.78)
Breakout acima W1: +2 (price crossing above Wave1 top)
MACD hist virando +: +1 (momentum confirmando reversão)
RSI < 50 no W2: +1 (RSI baixo no fundo de W2 = entrada ideal)
W1 impulsivo (ratio): +1 (W1 corpo > ATR * 3 = movimento real)
Volume W1 > W2: +1 (volume confirma — Frost & Prechter)
"""
# Busca o padrão Low-High-Low nos últimos pivôs
w0, w1h, w2l = None, None, None
for i in range(len(pivots) - 1, -1, -1):
pv = pivots[i]
if pv[1] == 'L' and w2l is None:
w2l = pv
elif pv[1] == 'H' and w2l is not None and w1h is None:
w1h = pv
elif pv[1] == 'L' and w1h is not None and w0 is None:
w0 = pv
break
if w0 is None or w1h is None or w2l is None:
return 0, None, None, None, 0, 0
w0_price = w0[2]
w1h_price = w1h[2]
w2l_price = w2l[2]
# Tamanho da Onda 1
w1_length = w1h_price - w0_price
if w1_length <= 0:
return 0, None, None, None, 0, 0
# Retracement de Wave2 em relação a Wave1
retrace_pct = (w1h_price - w2l_price) / w1_length
# Proporção mínima de W1 (evitar pivôs insignificantes)
if w1_length < atr * 2:
return 0, None, None, None, 0, 0
score = 0
# R1: Wave2 não ultrapassa início de Wave1 (REGRA INVIOLÁVEL)
if w2l_price > w0_price:
score += 2
else:
return 0, None, None, None, 0, 0 # Violou R1 — inválido
# Fib Wave2: retrace entre wave2_fib_min e wave2_fib_max
w2_min = self.wave2_fib_min.value
w2_max = self.wave2_fib_max.value
if w2_min <= retrace_pct <= w2_max:
score += 2
elif retrace_pct < w2_min or retrace_pct > w2_max + 0.15:
return 0, None, None, None, 0, 0 # Retrace muito raso ou muito fundo
# Breakout: preço atual acima do topo de W1 (Wave3 confirmado)
if price >= w1h_price * 0.998:
score += 2
elif price >= w1h_price * 0.985:
score += 1
# Não pontua se preço ainda está abaixo de W1 (possível entrada antecipada)
# MACD hist virando positivo (momentum de reversão)
if macd_hist > 0:
score += 1
# RSI baixo no W2 (fundo de Wave2 deve ser RSI < 50)
if rsi < self.rsi_w2_max.value:
score += 1
# W1 é um movimento impulsivo real (comprimento >= 3x ATR)
if w1_length >= atr * 3:
score += 1
# Calcular TP e SL
# Target Wave3: W2_low + Wave1_length * wave3_ext (Frost & Prechter: 1.618x)
tp = w2l_price + w1_length * self.wave3_ext.value
# Stop: abaixo do fundo de Wave2 - buffer ATR
sl = w2l_price - atr * self.stop_buffer_atr.value
return score, w0, w1h, w2l, tp, sl
def _check_wave5_setup(self, pivots, price, rsi, macd_hist, atr):
"""
Valida Setup B — Início da Onda 5.
Procura o padrão: W0(L) → W1(H) → W2(L) → W3(H) → W4(L)
e checa as 3 regras invioláveis.
SCORE componentes (máx ~10):
R2 validation (W3 > W1): +2 (Wave3 nunca é a mais curta)
R3 validation (W4 > W1_top): +2 (Wave4 não entra em W1 territory)
Fib W4 em zona ideal: +2 (23.6%-38.2% de W3 — guideline)
MACD hist virando +: +1
RSI divergência (W5 < W3): +1 (classic bearish divergence at W5)
W3 mais longa que W1: +1 (típico — W3 = 1.618 * W1)
Preço acima de W3 high: +1 (Wave5 breakout confirmado)
"""
# Busca o padrão Low-High-Low-High-Low nos últimos pivôs
w0, w1h, w2l, w3h, w4l = None, None, None, None, None
for i in range(len(pivots) - 1, -1, -1):
pv = pivots[i]
if pv[1] == 'L' and w4l is None:
w4l = pv
elif pv[1] == 'H' and w4l is not None and w3h is None:
w3h = pv
elif pv[1] == 'L' and w3h is not None and w2l is None:
w2l = pv
elif pv[1] == 'H' and w2l is not None and w1h is None:
w1h = pv
elif pv[1] == 'L' and w1h is not None and w0 is None:
w0 = pv
break
if w0 is None or w1h is None or w2l is None or w3h is None or w4l is None:
return 0, None, None, 0, 0
w0_p = w0[2]
w1h_p = w1h[2]
w2l_p = w2l[2]
w3h_p = w3h[2]
w4l_p = w4l[2]
w1_len = w1h_p - w0_p
w3_len = w3h_p - w2l_p
w4_retrace = (w3h_p - w4l_p) / (w3_len + 1e-9)
if w1_len <= 0 or w3_len <= 0:
return 0, None, None, 0, 0
# Proporção mínima para movimentos reais
if w1_len < atr * 2 or w3_len < atr * 2:
return 0, None, None, 0, 0
score = 0
# R2: Wave3 NÃO é a mais curta (deve ser >= Wave1)
if w3_len >= w1_len:
score += 2
else:
return 0, None, None, 0, 0 # Violou R2
# R3: Wave4 NÃO entra no território de Wave1 (W4_low > W1_high)
if w4l_p > w1h_p:
score += 2
else:
return 0, None, None, 0, 0 # Violou R3
# Fib Wave4: retrace entre wave4_fib_min e wave4_fib_max
w4_min = self.wave4_fib_min.value
w4_max = self.wave4_fib_max.value
if w4_min <= w4_retrace <= w4_max:
score += 2
elif w4_retrace > w4_max + 0.15:
return 0, None, None, 0, 0 # Too deep for Wave4
# MACD hist virando positivo
if macd_hist > 0:
score += 1
# Wave3 claramente maior que Wave1 (regra de extensão)
if w3_len >= w1_len * 1.3:
score += 1
# Preço deve estar acima de W3 high para Wave5 confirmação parcial
if price >= w3h_p * 0.995:
score += 1
# RSI pode mostrar divergência (Wave5 geralmente não atinge o RSI peak de Wave3)
if rsi < 70: # Se RSI não está sobrecomprado extremo, pode ter mais espaço
score += 1
# Target Wave5: W4_low + W1_length (regra da igualdade — Frost & Prechter)
tp = w4l_p + w1_len
# Stop: abaixo de Wave4 low
sl = w4l_p - atr * self.stop_buffer_atr.value
return score, w3h, w4l, tp, sl
# ===================================================================
# SINAIS DE COMPRA
# ===================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
score_min = self.buy_score_min.value
# Setup A: Wave 3 Onset
cond_a = (
self.use_wave3_setup.value &
(dataframe['ew_setup_a_score'] >= score_min) &
(dataframe['close'] > dataframe['ema50']) & # Tendência macro bullish
(dataframe['volume'] > 0) &
(dataframe['ew_target_w3'] > dataframe['close']) # TP acima do preço atual
)
# Setup B: Wave 5 Entry
cond_b = (
self.use_wave5_setup.value &
(dataframe['ew_setup_b_score'] >= score_min - 1) & # Wave5 é mais seguro, score menor
(dataframe['close'] > dataframe['ema50']) &
(dataframe['volume'] > 0) &
(dataframe['ew_target_w5'] > dataframe['close'])
)
dataframe.loc[cond_a | cond_b, 'enter_long'] = 1
return dataframe
# ===================================================================
# SINAIS DE SAÍDA
# ===================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(dataframe['rsi'] > 80) | # RSI sobrecomprado extremo
(dataframe['close'] < dataframe['ema20']) | # Perdeu EMA20
(dataframe['macd_hist'] < -0.0001), # MACD hist virou negativo forte
'exit_long'
] = 1
return dataframe
# ===================================================================
# CUSTOM STOPLOSS — ATR dinâmico baseado em Wave structure
# ===================================================================
def custom_stoploss(self, pair: str, trade: 'Trade', current_time,
current_rate: float, current_profit: float, **kwargs) -> float:
"""
Stop dinâmico baseado na estrutura Elliott Wave.
- Stop inicial: abaixo do fundo de Wave2 (Setup A) ou Wave4 (Setup B)
- Break-even em 1R de profit
- Progressivo trailing: lock 50% do ganho em 1.5R
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or dataframe.empty:
return -0.05
last_bar = dataframe.iloc[-1]
atr = float(last_bar['atr']) if not np.isnan(last_bar['atr']) else current_rate * 0.01
# Stop estrutural baseado nos níveis Elliott
ew_stop = float(last_bar['ew_stop']) if not np.isnan(last_bar['ew_stop']) else 0
if ew_stop > 0:
# Stop estrutural: abaixo do nível de Wave2 ou Wave4
structural_stop = (ew_stop - current_rate) / current_rate
structural_stop = max(structural_stop, -0.08) # Cap em -8%
else:
structural_stop = -0.05
# Break-even: se lucro >= 1R, stop em +0.1%
r_unit = abs(structural_stop)
if current_profit >= r_unit * 1.0:
return -0.001 # Break-even
# Lock parcial: se lucro >= 1.5R, trailing de 50% do ganho
if current_profit >= r_unit * 1.5:
return stoploss_from_open(
-(current_profit * 0.5),
current_profit
)
return structural_stop
# ===================================================================
# CUSTOM EXIT — TP baseado em projeção Fibonacci de Wave3 / Wave5
# ===================================================================
def custom_exit(self, pair: str, trade: 'Trade', current_time,
current_rate: float, current_profit: float,
**kwargs) -> Optional[str]:
"""
Saída customizada com TP nos níveis Elliott Wave calculados.
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or dataframe.empty:
return None
last_bar = dataframe.iloc[-1]
# TP Wave 3: target calculado na entrada
tp_w3 = float(last_bar['ew_target_w3']) if not np.isnan(last_bar['ew_target_w3']) else 0
if tp_w3 > 0 and current_rate >= tp_w3:
return "wave3_target_reached"
# TP Wave 5
tp_w5 = float(last_bar['ew_target_w5']) if not np.isnan(last_bar['ew_target_w5']) else 0
if tp_w5 > 0 and current_rate >= tp_w5:
return "wave5_target_reached"
# TP via R:R ratio (fallback)
atr = float(last_bar['atr']) if not np.isnan(last_bar['atr']) else 0
ew_stop = float(last_bar['ew_stop']) if not np.isnan(last_bar['ew_stop']) else 0
if ew_stop > 0 and atr > 0:
risk = trade.open_rate - ew_stop
if risk > 0:
tp_rr = trade.open_rate + risk * self.tp_rr_ratio.value
if current_rate >= tp_rr:
return f"rr_{self.tp_rr_ratio.value:.1f}_reached"
# Time-based exit: 24h máximo
open_duration = (current_time - trade.open_date_utc).seconds / 3600
if open_duration > 24:
return "time_exit_24h"
return None