Crypto KMLM Switcher Strategy - ADVANCED VERSION
Timeframe
5m
Direction
Long Only
Stoploss
-90.0%
Trailing Stop
Yes
ROI
0m: 18.0%, 30m: 12.0%, 60m: 9.0%, 120m: 6.0%
Interface Version
3
Startup Candles
N/A
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from typing import Dict, List
from functools import reduce
from freqtrade.strategy import (
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IStrategy,
IntParameter,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import merge_informative_pair
class CryptoKMLMSwitcherAdvanced(IStrategy):
"""
Crypto KMLM Switcher Strategy - ADVANCED VERSION
Implements the sophisticated switching logic from defsymphony.clj
"Simons KMLM switcher (single pops)| BT 4/13/22 = A.R. 466% / D.D. 22%"
Core Logic:
1. Multi-pair RSI monitoring (like the original's multi-asset approach)
2. Defensive switching to stable pairs when major pairs are overbought
3. Aggressive switching to volatile pairs when major pairs are oversold
4. Dynamic position sizing based on market conditions
5. Sophisticated nested decision tree for allocation
Key Features:
- Defensive mode: Switch to stable pairs (like UVXY in original)
- Aggressive mode: Switch to volatile pairs (like TECL, SOXL, SPXL)
- Risk management through RSI thresholds
- Multi-tier decision tree for optimal allocation
"""
INTERFACE_VERSION = 3
timeframe = "5m" # Optimal timeframe for crypto switching
# ROI table - more realistic for crypto volatility
minimal_roi = {
"0": 0.18, # 6% profit target
"30": 0.12, # 4% after 30 minutes
"60": 0.09, # 3% after 1 hour
"120": 0.06, # 2% after 2 hours
"240": 0.045, # 1.5% after 4 hours
"480": 0.03, # 1% after 8 hours
}
# Stoploss - more appropriate for crypto volatility
stoploss = -0.90 # 90% stop loss - effectively disabled (let ROI and trailing stop handle exits)
use_custom_stoploss = False # Disable custom stoploss to avoid regime-based exits
# Disable shorting for spot trading
can_short = False
# Trailing stoploss - less aggressive
trailing_stop = True
trailing_stop_positive = 0.015 # 1.5% trailing stop
trailing_stop_positive_offset = 0.025 # 2.5% offset
trailing_only_offset_is_reached = True
# Run "populate_indicators()" only for new candle
process_only_new_candles = True
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 30
# Strategy parameters - optimized for crypto
rsi_window = IntParameter(10, 15, default=12, space="buy")
rsi_overbought_high = IntParameter(75, 85, default=78, space="sell")
rsi_overbought_medium = IntParameter(70, 80, default=72, space="sell")
rsi_oversold = IntParameter(20, 40, default=35, space="buy") # Relaxed from 28 to 35
# Volume confirmation
volume_multiplier = DecimalParameter(1.0, 2.0, default=1.2, space="buy") # Relaxed from 1.5 to 1.2
# Position sizing parameters
max_risk_per_trade = DecimalParameter(0.02, 0.08, default=0.05, space="buy")
defensive_allocation = DecimalParameter(0.3, 0.7, default=0.5, space="buy")
aggressive_allocation = DecimalParameter(0.6, 0.9, default=0.8, space="buy")
# Exit signal parameters - easily editable
enable_exit_signals = False # Set to False to disable exit signals (main source of losses)
exit_rsi_threshold = 92 # RSI level for exit (higher = less exits)
exit_trend_drop = 0.88 # Price drop below SMA20 for trend exit (lower = less exits)
exit_extreme_drop = 0.80 # Extreme price drop for exit (lower = less exits)
exit_volume_spike = 2.5 # Volume ratio for panic selling exit (higher = less exits)
exit_candle_drop = 0.85 # Intra-candle drop for volume spike exit (lower = less exits)
# Regime change exit parameters - reduce these to minimize regime change losses
enable_regime_exits = False # Set to False to disable regime change exits (bleeding the most)
regime_exit_profit_threshold = 0.12 # Exit defensive positions when aggressive (higher = less exits)
regime_exit_loss_threshold = -0.20 # Exit aggressive positions when defensive (lower = less exits)
# Timeout parameters
enable_trade_timeout = False # Set to False to disable timeout exits
trade_timeout_hours = 72 # Hours before timeout exit (higher = longer trades)
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These represent the "market leaders" that drive switching decisions.
"""
return [
("BTC/USDT", "5m"), # Market leader (like SPY in original)
("ETH/USDT", "5m"), # Tech leader (like QQQE in original)
("BNB/USDT", "5m"), # Exchange token (like XLK in original)
("ADA/USDT", "5m"), # Volatile alt (like TECL in original)
("DOT/USDT", "5m"), # Volatile alt (like SOXL in original)
("SOL/USDT", "5m"), # Volatile alt (like SPXL in original)
#("AVAX/USDT", "5m"), # Volatile alt (like LABU in original)
]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
"""
# RSI
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.rsi_window.value)
# Moving averages for trend detection
dataframe['sma_20'] = ta.SMA(dataframe, timeperiod=20)
dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50)
dataframe['sma_200'] = ta.SMA(dataframe, timeperiod=200)
# Bollinger Bands
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_percent'] = (dataframe['close'] - dataframe['bb_lowerband']) / (dataframe['bb_upperband'] - dataframe['bb_lowerband'])
# Volume indicators
dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean()
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_mean']
# Market conditions
dataframe['bullish_trend'] = dataframe['sma_20'] > dataframe['sma_50']
dataframe['bearish_trend'] = dataframe['sma_20'] < dataframe['sma_50']
dataframe['strong_trend'] = dataframe['sma_50'] > dataframe['sma_200']
# RSI conditions
dataframe['rsi_oversold'] = dataframe['rsi'] < self.rsi_oversold.value
dataframe['rsi_overbought_medium'] = dataframe['rsi'] > self.rsi_overbought_medium.value
dataframe['rsi_overbought_high'] = dataframe['rsi'] > self.rsi_overbought_high.value
# Volatility
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['high_volatility'] = dataframe['atr'] > dataframe['atr'].rolling(window=20).mean() * 1.5
# Market regime detection
dataframe['market_regime'] = self.detect_market_regime(dataframe)
return dataframe
def detect_market_regime(self, dataframe: DataFrame) -> pd.Series:
"""
Detect market regime based on RSI and volatility
Returns: 0 = Defensive, 1 = Neutral, 2 = Aggressive
"""
regime = pd.Series(1, index=dataframe.index) # Default to neutral
# Defensive regime: High RSI + High volatility
defensive_condition = (
(dataframe['rsi_overbought_high']) &
(dataframe['high_volatility'])
)
regime[defensive_condition] = 0
# Aggressive regime: Low RSI + Strong trend
aggressive_condition = (
(dataframe['rsi_oversold']) &
(dataframe['strong_trend']) &
(dataframe['volume_ratio'] > self.volume_multiplier.value)
)
regime[aggressive_condition] = 2
return regime
def get_market_rsis(self, metadata: dict) -> Dict[str, float]:
"""
Get RSI values from major pairs to determine switching conditions
"""
market_rsis = {}
# Define the pairs we want to monitor (like the original strategy)
pairs_to_monitor = [
'BTC/USDT', 'ETH/USDT', 'BNB/USDT',
'ADA/USDT', 'DOT/USDT', 'SOL/USDT'#, 'AVAX/USDT' binance us does not trade this
]
for pair in pairs_to_monitor:
if pair != metadata['pair']:
try:
pair_dataframe = self.dp.get_pair_dataframe(pair, self.timeframe)
if pair_dataframe is not None and len(pair_dataframe) > 0:
# Calculate RSI for this pair
pair_rsi = ta.RSI(pair_dataframe, timeperiod=self.rsi_window.value)
market_rsis[pair] = pair_rsi.iloc[-1] if not pd.isna(pair_rsi.iloc[-1]) else 50
except:
market_rsis[pair] = 50 # Default if data unavailable
return market_rsis
def determine_position_size(self, dataframe: DataFrame, metadata: dict) -> float:
"""
Determine position size based on market regime and RSI conditions
Implements the sophisticated allocation logic from the original strategy
"""
# Get market RSI values
market_rsis = self.get_market_rsis(metadata)
# Count how many pairs are overbought/oversold
overbought_count = sum(1 for rsi in market_rsis.values() if rsi > self.rsi_overbought_high.value)
oversold_count = sum(1 for rsi in market_rsis.values() if rsi < self.rsi_oversold.value)
# Get current pair's RSI
current_rsi = dataframe['rsi'].iloc[-1]
# Determine allocation based on market conditions
if overbought_count >= 3: # Defensive mode (like UVXY in original)
return self.defensive_allocation.value
elif oversold_count >= 3 and current_rsi < self.rsi_oversold.value: # Aggressive mode
return self.aggressive_allocation.value
else: # Neutral mode
return 0.6 # Default allocation
return 0.5 # Fallback
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Implements the sophisticated nested decision tree from the original strategy
"""
# Get market RSI values
market_rsis = self.get_market_rsis(metadata)
# Initialize entry conditions
long_conditions = []
# Count overbought/oversold pairs for better decision making
overbought_count = sum(1 for rsi in market_rsis.values() if rsi > self.rsi_overbought_high.value)
oversold_count = sum(1 for rsi in market_rsis.values() if rsi < self.rsi_oversold.value)
# Base entry condition - always check for oversold RSI
base_condition = (
(dataframe['rsi_oversold']) &
(dataframe['volume'] > 0)
)
# Defensive mode: When major pairs are overbought, prefer stable pairs
defensive_mode = overbought_count >= 1 # Relaxed from 2 to 1
# Aggressive mode: When many pairs are oversold, prefer volatile pairs
aggressive_mode = oversold_count >= 1 # Relaxed from 2 to 1
# Entry logic with pair-specific conditions
if metadata['pair'] in ['BTC/USDT', 'ETH/USDT', 'BNB/USDT']:
# Stable pairs - enter in defensive mode or when oversold
if defensive_mode:
# Defensive entry for stable pairs
defensive_entry = (
base_condition &
(dataframe['volume_ratio'] > 0.8) # Relaxed from 1.1 to 0.8
)
long_conditions.append(defensive_entry)
else:
# Normal entry for stable pairs
stable_entry = (
base_condition &
(dataframe['volume_ratio'] > 0.9) # Relaxed from 1.2 to 0.9
)
long_conditions.append(stable_entry)
elif metadata['pair'] in ['ADA/USDT', 'DOT/USDT', 'SOL/USDT']:
# Volatile pairs - enter in aggressive mode or when oversold
if aggressive_mode:
# Aggressive entry for volatile pairs
aggressive_entry = (
base_condition &
(dataframe['volume_ratio'] > 1.0) # Relaxed from volume_multiplier to 1.0
)
long_conditions.append(aggressive_entry)
else:
# Normal entry for volatile pairs
volatile_entry = (
base_condition &
(dataframe['volume_ratio'] > 1.0) # Relaxed from 1.3 to 1.0
)
long_conditions.append(volatile_entry)
else:
# Other pairs - balanced approach
balanced_entry = (
base_condition &
(dataframe['volume_ratio'] > 0.9) # Relaxed from 1.2 to 0.9
)
long_conditions.append(balanced_entry)
# Combine all conditions
if long_conditions:
dataframe.loc[reduce(lambda x, y: x | y, long_conditions), 'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Exit logic - conservative exit signals that work with ROI and trailing stop
"""
# Disable exit signals if parameter is False
if not self.enable_exit_signals:
dataframe['exit_long'] = 0
dataframe['exit_short'] = 0
return dataframe
# Exit long positions - only on very strong reversal signals
# This should be much less frequent than ROI/trailing stop exits
dataframe.loc[
(
# Exit only on extreme RSI overbought (very high threshold)
(dataframe['rsi'] > self.exit_rsi_threshold) |
# Exit only on strong bearish trend AND significant price drop
(dataframe['bearish_trend'] & (dataframe['close'] < dataframe['sma_20'] * self.exit_trend_drop)) |
# Exit only on extreme reversal (very significant drop)
(dataframe['close'] < dataframe['sma_20'] * self.exit_extreme_drop) |
# Exit on volume spike with price drop (panic selling)
((dataframe['volume_ratio'] > self.exit_volume_spike) & (dataframe['close'] < dataframe['open'] * self.exit_candle_drop))
) &
(dataframe['volume'] > 0),
'exit_long'
] = 1
return dataframe
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> float:
"""
Custom stoploss logic based on market regime
"""
# Get the dataframe for this pair
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) > 0:
# Get current market regime
current_regime = dataframe['market_regime'].iloc[-1]
# Adjust stoploss based on regime
if current_regime == 0: # Defensive
return -0.05 # Tighter stoploss
elif current_regime == 2: # Aggressive
return -0.15 # Wider stoploss
else: # Neutral
return self.stoploss
return self.stoploss
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> str:
"""
Custom exit logic based on market regime changes - less aggressive
"""
# Disable regime exits if parameter is False
if not self.enable_regime_exits:
# Only check timeout if regime exits are disabled
if self.enable_trade_timeout and (current_time - trade.open_date_utc).total_seconds() > (self.trade_timeout_hours * 3600):
return "timeout_exit"
return None
# Get the dataframe for this pair
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if len(dataframe) > 0:
# Exit if market regime changes significantly
current_regime = dataframe['market_regime'].iloc[-1]
# Exit defensive positions when market becomes aggressive (higher profit threshold)
if current_regime == 2 and current_profit > self.regime_exit_profit_threshold:
return "regime_change_exit"
# Exit aggressive positions when market becomes defensive (lower loss threshold)
if current_regime == 0 and current_profit < self.regime_exit_loss_threshold:
return "regime_change_exit"
# Exit if we've been in the trade too long (configurable timeout)
if self.enable_trade_timeout and (current_time - trade.open_date_utc).total_seconds() > (self.trade_timeout_hours * 3600):
return "timeout_exit"
return None