Timeframe
15m
Direction
Long Only
Stoploss
-4.0%
Trailing Stop
No
ROI
0m: 4.0%, 30m: 2.5%, 60m: 1.5%, 120m: 0.0%
Interface Version
3
Startup Candles
500
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
FreqForge_Override — Baseline v1
Basiert auf RegimeSwitchingHybrid_v7_v04_Integration
Nur ROI + Hard Stoploss (-9%).
Kein Trailing, keine Exit-Signale, kein LLM-Layer.
Shadow-JSONL-Logging in freqforge_shadow.log (passiv).
"""
import logging
import sys
import json
import os
from datetime import datetime
from typing import Optional
import talib.abstract as ta
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, merge_informative_pair
import freqtrade.vendor.qtpylib.indicators as qtpylib
from pandas import DataFrame
sys.path.insert(0, "/freqtrade/shared")
from primo_signal import primo_gate_allows
logger = logging.getLogger(__name__)
class FreqForge_Override(IStrategy):
INTERFACE_VERSION = 3
timeframe = "15m"
informative_timeframe = "1h"
can_short = False
minimal_roi = {'0': 0.04, '30': 0.025, '60': 0.015, '120': 0}
stoploss = -0.04
use_custom_stoploss = False
trailing_stop = False
startup_candle_count = 500
@property
def protections(self):
return [
{"method": "CooldownPeriod", "stop_duration_candles": 5},
{"method": "StoplossGuard", "lookback_period_candles": 60,
"trade_limit": 3, "stop_duration_candles": 60,
"only_per_pair": False, "only_per_side": True},
{"method": "MaxDrawdown", "lookback_period_candles": 480,
"trade_limit": 20, "stop_duration_candles": 96,
"max_allowed_drawdown": 0.06},
{"method": "LowProfitPairs", "lookback_period_candles": 1440,
"trade_limit": 2, "stop_duration_candles": 60,
"required_profit": -0.01},
]
adx_rel_threshold = DecimalParameter(0.8, 1.4, default=1.0, space="buy")
rsi_oversold = IntParameter(20, 40, default=25, space="buy")
def __init__(self, config: dict) -> None:
super().__init__(config)
self._regime_histories: dict = {}
def _get_stable_regime(self, pair: str, current_regime: str) -> str:
"""2-cycle hysteresis per pair. Regime shifts only after 2 consecutive same candles."""
if pair not in self._regime_histories:
self._regime_histories[pair] = []
history = self._regime_histories[pair]
history.append(current_regime)
if len(history) > 2:
history.pop(0)
if len(history) == 2 and history[0] == history[1]:
return history[1]
return history[0] if history else "unknown"
def informative_pairs(self):
pairs = self.dp.current_whitelist()
return [(pair, self.informative_timeframe) for pair in pairs]
def _build_v04_signal_layer(self, dataframe: DataFrame, pair: str) -> None:
"""Pre-compute v0.4 signals as vectorized columns. Uses per-pair regime tracking."""
dataframe['prev_close'] = dataframe['close'].shift(1)
# Path 1: Mean Reversion
rsi_buy = dataframe['rsi'] <= 30
rsi_sell = dataframe['rsi'] >= 70
mean_rev_active = rsi_buy | rsi_sell
# Path 2: Trend Following
trend_active = (dataframe['adx'] > 22) & dataframe['ema50'].notna() & dataframe['ema200'].notna()
trend_buy = trend_active & (dataframe['ema50'] > dataframe['ema200'])
trend_sell = trend_active & (dataframe['ema50'] <= dataframe['ema200'])
# Path 3: Breakout
breakout_active = (dataframe['bb_width'] < 0.5) & (dataframe['volume_ratio'] > 1.05)
breakout_buy = breakout_active & dataframe['close'].gt(dataframe['prev_close'])
breakout_sell = breakout_active & dataframe['close'].le(dataframe['prev_close'])
# Initialize all as WATCH
dataframe['v04_action'] = 'WATCH'
dataframe['v04_confidence'] = 0.0
dataframe['v04_strategy'] = None
dataframe['v04_regime'] = 'unknown'
# Per-pair stable regime (last candle)
if len(dataframe) > 0:
last_row = dataframe.iloc[-1]
adx_val = last_row.get('adx', 0)
vol_r = last_row.get('volume_ratio', 1.0)
raw_regime = "trending" if adx_val > 22 else "ranging"
if vol_r > 1.8:
raw_regime = "volatile"
stable_regime = self._get_stable_regime(pair, raw_regime)
dataframe['v04_regime'] = stable_regime
# Priority: MEAN_REVERSION > TREND > BREAKOUT > WATCH
mr_buy_mask = mean_rev_active & rsi_buy
mr_sell_mask = mean_rev_active & rsi_sell
trend_buy_mask = trend_buy & ~mean_rev_active
trend_sell_mask = trend_sell & ~mean_rev_active
breakout_buy_mask = breakout_buy & ~mean_rev_active & ~trend_buy
breakout_sell_mask = breakout_sell & ~mean_rev_active & ~trend_sell
# MEAN REVERSION
dataframe.loc[mr_buy_mask, 'v04_action'] = 'BUY'
dataframe.loc[mr_buy_mask, 'v04_strategy'] = 'MEAN_REVERSION'
ext = (dataframe.loc[mr_buy_mask, 'rsi'] - 30).abs() / 10
dataframe.loc[mr_buy_mask, 'v04_confidence'] = ext.clip(0, 1).round(4)
dataframe.loc[mr_sell_mask, 'v04_action'] = 'SELL'
dataframe.loc[mr_sell_mask, 'v04_strategy'] = 'MEAN_REVERSION'
ext = (dataframe.loc[mr_sell_mask, 'rsi'] - 70).abs() / 10
dataframe.loc[mr_sell_mask, 'v04_confidence'] = ext.clip(0, 1).round(4)
# TREND FOLLOWING
dataframe.loc[trend_buy_mask, 'v04_action'] = 'BUY'
dataframe.loc[trend_buy_mask, 'v04_strategy'] = 'TREND_FOLLOWING'
adx_raw = ((dataframe.loc[trend_buy_mask, 'adx'] - 22) / 18).clip(0, 1)
rsi_factor = ((dataframe.loc[trend_buy_mask, 'rsi'] >= 35) &
(dataframe.loc[trend_buy_mask, 'rsi'] <= 65)).astype(float) * 0.3 + 0.7
vol_factor = 1.1 * (dataframe.loc[trend_buy_mask, 'volume_ratio'] > 1.1).astype(float) + \
0.9 * (dataframe.loc[trend_buy_mask, 'volume_ratio'] <= 1.1).astype(float)
dataframe.loc[trend_buy_mask, 'v04_confidence'] = (adx_raw * rsi_factor * vol_factor).clip(0, 1).round(4)
dataframe.loc[trend_sell_mask, 'v04_action'] = 'SELL'
dataframe.loc[trend_sell_mask, 'v04_strategy'] = 'TREND_FOLLOWING'
adx_raw = ((dataframe.loc[trend_sell_mask, 'adx'] - 22) / 18).clip(0, 1)
rsi_factor = ((dataframe.loc[trend_sell_mask, 'rsi'] >= 35) &
(dataframe.loc[trend_sell_mask, 'rsi'] <= 65)).astype(float) * 0.3 + 0.7
vol_factor = 1.1 * (dataframe.loc[trend_sell_mask, 'volume_ratio'] > 1.1).astype(float) + \
0.9 * (dataframe.loc[trend_sell_mask, 'volume_ratio'] <= 1.1).astype(float)
dataframe.loc[trend_sell_mask, 'v04_confidence'] = (adx_raw * rsi_factor * vol_factor).clip(0, 1).round(4)
# BREAKOUT
dataframe.loc[breakout_buy_mask, 'v04_action'] = 'BUY'
dataframe.loc[breakout_buy_mask, 'v04_strategy'] = 'BREAKOUT'
squeeze = ((0.5 - dataframe.loc[breakout_buy_mask, 'bb_width']) / 0.4).clip(0, 1)
vol = (dataframe.loc[breakout_buy_mask, 'volume_ratio'] / 1.5).clip(0, 1)
dataframe.loc[breakout_buy_mask, 'v04_confidence'] = (0.5 * squeeze + 0.5 * vol).round(4)
dataframe.loc[breakout_sell_mask, 'v04_action'] = 'SELL'
dataframe.loc[breakout_sell_mask, 'v04_strategy'] = 'BREAKOUT'
squeeze = ((0.5 - dataframe.loc[breakout_sell_mask, 'bb_width']) / 0.4).clip(0, 1)
vol = (dataframe.loc[breakout_sell_mask, 'volume_ratio'] / 1.5).clip(0, 1)
dataframe.loc[breakout_sell_mask, 'v04_confidence'] = (0.5 * squeeze + 0.5 * vol).round(4)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if not self.dp:
return dataframe
pair = metadata.get("pair", "UNKNOWN")
informative = self.dp.get_pair_dataframe(
pair=pair, timeframe=self.informative_timeframe
)
informative['ema200'] = ta.EMA(informative, timeperiod=200)
informative['adx'] = ta.ADX(informative)
informative['rsi'] = ta.RSI(informative)
dataframe = merge_informative_pair(
dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True
)
dataframe['adx'] = ta.ADX(dataframe)
dataframe['adx_sma'] = dataframe['adx'].rolling(window=50).mean()
dataframe['adx_rel'] = dataframe['adx'] / dataframe['adx_sma']
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['atr_pct'] = dataframe['atr'] / dataframe['close']
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=20, stds=2
)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
dataframe['bb_width'] = (bollinger['upper'] - bollinger['lower']) / bollinger['mid']
dataframe['volume_mean'] = dataframe['volume'].rolling(window=30).mean()
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_mean']
# v0.4 signal pre-computation (vectorized, per-pair regime tracking)
self._build_v04_signal_layer(dataframe, pair)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
ema200_htf = dataframe[f'ema200_{self.informative_timeframe}']
pair = metadata.get("pair")
long_gate = primo_gate_allows(pair, "long")
# --- Strategy-native LONG conditions ---
trend_long = (
(dataframe['adx_rel'] > self.adx_rel_threshold.value) &
(dataframe['close'] > ema200_htf) &
(dataframe['close'] > dataframe['ema200']) &
(dataframe['close'] < dataframe['ema50']) &
(dataframe['rsi'] < 50) &
(dataframe['volume'] > dataframe['volume_mean']) &
long_gate
)
range_long = (
(dataframe['adx_rel'] <= self.adx_rel_threshold.value) &
(dataframe['rsi'] < self.rsi_oversold.value) &
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['volume'] > dataframe['volume_mean']) &
long_gate
)
# --- v0.4 SECOND LAYER: Override via v04_action column ---
long_override_mask = ((dataframe['v04_action'] == 'WATCH') & (trend_long | range_long))
# Apply combined entries
long_entries = (trend_long | range_long) & ~long_override_mask
dataframe.loc[long_entries, 'enter_long'] = 1
dataframe.loc[long_entries, 'enter_tag'] = 'range_reversion_long'
dataframe.loc[trend_long & ~long_override_mask, 'enter_tag'] = 'trend_pullback_long'
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Nur ROI + Hard Stoploss — keine Exit-Signale
return dataframe
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], side: str, **kwargs) -> bool:
"""
PASSIVES SHADOW-LOGGING — zeichnet jeden Trade mit Marktkontext auf.
Gibt immer True zurueck (kein Eingriff in Trades).
"""
log_entry = {
"timestamp": current_time.isoformat(),
"pair": pair,
"rate": float(rate),
"side": side,
"entry_tag": entry_tag,
"amount": float(amount),
"strategy": "FreqForge_Override",
"config": "baseline-v1",
}
# DataFrame-Kontext aus der letzten Kerze holen
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is not None and not dataframe.empty:
last = dataframe.iloc[-1]
log_entry["close"] = float(last.get("close", 0))
log_entry["rsi"] = float(last.get("rsi", 0))
log_entry["adx"] = float(last.get("adx", 0))
log_entry["adx_rel"] = float(last.get("adx_rel", 1.0))
log_entry["atr_pct"] = float(last.get("atr_pct", 0))
log_entry["bb_width"] = float(last.get("bb_width", 0))
log_entry["v04_action"] = str(last.get("v04_action", "N/A"))
log_entry["v04_confidence"] = float(last.get("v04_confidence", 0))
log_entry["v04_strategy"] = str(last.get("v04_strategy", "N/A"))
log_entry["v04_regime"] = str(last.get("v04_regime", "N/A"))
log_entry["volume_ratio"] = float(
last.get("volume", 0) / last.get("volume_mean", 1)
if last.get("volume_mean", 0) > 0 else 0
)
except Exception as e:
log_entry["context_error"] = str(e)
# JSONL-Log schreiben
try:
log_path = "/freqtrade/logs/freqforge_shadow.log"
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "a") as f:
f.write(json.dumps(log_entry) + "\n")
except Exception as e:
logger.error(f"FreqForge Shadow log write failed: {e}")
return True