Replica las reglas de GeminiStrategy sin llamadas a API. Útil para backtesting rápido con datos históricos reales.
Timeframe
5m
Direction
Long Only
Stoploss
-1.5%
Trailing Stop
Yes
ROI
0m: 4.0%, 30m: 2.5%, 60m: 2.0%, 120m: 1.0%
Interface Version
3
Startup Candles
N/A
Indicators
6
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
GeminiBacktest — versión sin API para backtesting rápido.
Replica la lógica de entrada/salida de GeminiStrategy usando solo indicadores técnicos.
Usa las mismas reglas que Gemini aplica en producción: RSI, EMA, MACD, BB, volumen.
"""
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter
def _calc_ema(series: pd.Series, period: int) -> pd.Series:
return series.ewm(span=period, adjust=False).mean()
class GeminiBacktest(IStrategy):
"""
Replica las reglas de GeminiStrategy sin llamadas a API.
Útil para backtesting rápido con datos históricos reales.
"""
INTERFACE_VERSION = 3
timeframe = "5m"
can_short = False
max_open_trades = 5
stoploss = -0.015
minimal_roi = {"0": 0.04, "30": 0.025, "60": 0.02, "120": 0.01}
trailing_stop = True
trailing_stop_positive = 0.006
trailing_stop_positive_offset = 0.015
trailing_only_offset_is_reached = True
startup_candle_count: int = 210
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["rsi"] = dataframe["close"].diff().pipe(
lambda d: pd.concat([
d.clip(lower=0).rolling(14).mean(),
(-d.clip(upper=0)).rolling(14).mean()
], axis=1).apply(lambda r: 100 - 100 / (1 + r.iloc[0] / r.iloc[1]) if r.iloc[1] != 0 else 50, axis=1)
)
dataframe["ema20"] = _calc_ema(dataframe["close"], 20)
dataframe["ema50"] = _calc_ema(dataframe["close"], 50)
dataframe["ema200"] = _calc_ema(dataframe["close"], 200)
ema12 = _calc_ema(dataframe["close"], 12)
ema26 = _calc_ema(dataframe["close"], 26)
dataframe["macd"] = ema12 - ema26
dataframe["macd_signal"] = _calc_ema(dataframe["macd"], 9)
dataframe["macd_hist"] = dataframe["macd"] - dataframe["macd_signal"]
bb_mid = dataframe["close"].rolling(20).mean()
bb_std = dataframe["close"].rolling(20).std()
dataframe["bb_upper"] = bb_mid + 2 * bb_std
dataframe["bb_lower"] = bb_mid - 2 * bb_std
dataframe["bb_pct"] = (dataframe["close"] - dataframe["bb_lower"]) / (dataframe["bb_upper"] - dataframe["bb_lower"]) * 100
vol_ma = dataframe["volume"].rolling(20).mean()
dataframe["volume_ratio"] = dataframe["volume"] / vol_ma
high_low = dataframe["high"] - dataframe["low"]
high_close = (dataframe["high"] - dataframe["close"].shift()).abs()
low_close = (dataframe["low"] - dataframe["close"].shift()).abs()
true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
dataframe["atr"] = true_range.ewm(span=14, adjust=False).mean()
dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"] * 100
# ADX (14)
high_diff = dataframe["high"].diff()
low_diff = dataframe["low"].diff()
plus_dm = high_diff.where((high_diff > 0) & (high_diff > -low_diff), 0.0)
minus_dm = (-low_diff).where((-low_diff > 0) & (-low_diff > high_diff), 0.0)
atr14 = dataframe["atr"]
plus_di = 100 * (plus_dm.ewm(span=14, adjust=False).mean() / atr14.replace(0, 1e-10))
minus_di = 100 * (minus_dm.ewm(span=14, adjust=False).mean() / atr14.replace(0, 1e-10))
dx = (abs(plus_di - minus_di) / (plus_di + minus_di + 1e-10)) * 100
dataframe["adx"] = dx.ewm(span=14, adjust=False).mean()
# MFI (14)
typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
money_flow = typical_price * dataframe["volume"]
tp_prev = typical_price.shift(1)
pos_flow = money_flow.where(typical_price > tp_prev, 0.0).rolling(14).sum()
neg_flow = money_flow.where(typical_price < tp_prev, 0.0).rolling(14).sum()
mfi_ratio = pos_flow / neg_flow.replace(0, 1e-10)
dataframe["mfi"] = 100 - (100 / (1 + mfi_ratio))
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Reglas de entrada que replica lo que Gemini decide:
- Señal principal: tendencia alcista en todos los indicadores
- Señal oportunista: RSI sobreventa + BB bajo
"""
above_ema200 = dataframe["close"] > dataframe["ema200"]
ema_bull = dataframe["ema20"] > dataframe["ema50"]
macd_bull = dataframe["macd_hist"] > 0
rsi_ok = dataframe["rsi"] < 50
vol_ok = dataframe["volume_ratio"] > 1.0
bb_mid_zone = dataframe["bb_pct"] < 70
adx_ok = dataframe["adx"] > 25
señal_principal = above_ema200 & ema_bull & macd_bull & rsi_ok & vol_ok & bb_mid_zone & adx_ok
rsi_oversold = dataframe["rsi"] < 35
bb_low = dataframe["bb_pct"] < 25
vol_min = dataframe["volume_ratio"] > 0.8
mfi_oversold = dataframe["mfi"] < 25
señal_oportunista = rsi_oversold & bb_low & vol_min & mfi_oversold & adx_ok
dataframe.loc[señal_principal | señal_oportunista, "enter_long"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Reglas de salida:
- RSI sobrecomprado + MACD bajando
- Divergencia RSI (precio sube, RSI baja)
"""
rsi_high = dataframe["rsi"] > 70
macd_bear = dataframe["macd_hist"] < 0
rsi_prev = dataframe["rsi"].shift(3)
close_prev = dataframe["close"].shift(3)
rsi_divergence = (dataframe["close"] > close_prev) & (dataframe["rsi"] < rsi_prev - 5)
dataframe.loc[(rsi_high & macd_bear) | rsi_divergence, "exit_long"] = 1
return dataframe