Master Dual-Asset Selection Strategy for Kraken BTC/USD and ETH/USD trading.
Timeframe
1h
Direction
Long Only
Stoploss
-3.0%
Trailing Stop
No
ROI
0m: 8.0%, 60m: 6.0%, 120m: 4.0%, 240m: 1.0%
Interface Version
3
Startup Candles
N/A
Indicators
5
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
import pandas as pd
from pandas import DataFrame
from datetime import datetime
from typing import Optional, Union, List
from functools import reduce
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative,
merge_informative_pair,
stoploss_from_open,
stoploss_from_absolute,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
# Import our custom pair selector
from user_data.strategies.pair_selector import PairSelector, calculate_setup_indicators
class DualAssetSelectionStrategy(IStrategy):
"""
Master Dual-Asset Selection Strategy for Kraken BTC/USD and ETH/USD trading.
Implements the research-defined approach:
- Scans both BTC/USD and ETH/USD daily for best setups
- Uses 7+ out of 10 scoring quality gate
- Targets 60% BTC trades (swing), 40% ETH trades (momentum)
- Single $1000 position focus with 4-6% minimum profit targets
Based on Strategy Analysis Report and Kraken Trading Pairs Analysis.
"""
# Strategy interface version
INTERFACE_VERSION = 3
# Dynamic timeframe (adjusted based on selected pair)
timeframe = "1h" # Default, will be overridden by pair selection
# Can this strategy go short?
can_short = False
# Dynamic ROI (adjusted based on selected pair/strategy)
minimal_roi = {
"0": 0.08, # 8% target (adjusted per pair)
"60": 0.06, # 6% after 1 hour
"120": 0.04, # 4% after 2 hours (minimum for fees)
"240": 0.01, # 1% after 4 hours (safety exit)
}
# Dynamic stoploss (adjusted based on pair volatility)
stoploss = -0.03 # 3% default (2.5% for BTC, 3% for ETH)
# Trailing stoploss
trailing_stop = False
# Dual-asset selection parameters
startup_candle_count: int = 50 # Enough for all indicators
# Pair selection settings
quality_gate_threshold = 7.0 # 7+ out of 10 to trade
btc_allocation_target = 0.60 # 60% BTC trades
eth_allocation_target = 0.40 # 40% ETH trades
# Initialize pair selector
def __init__(self, config: dict) -> None:
super().__init__(config)
self.pair_selector = PairSelector()
self.latest_selection_result = None
def informative_pairs(self):
"""
Define informative pairs for both BTC and ETH analysis
"""
return [
("BTC/USD", "4h"), # BTC trend context
("ETH/USD", "4h"), # ETH trend context
]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Run all indicator calculations for the asset.
"""
pair = metadata["pair"]
asset_type = "BTC" if "BTC" in pair else "ETH"
# Use the centralized indicator calculation function
dataframe = calculate_setup_indicators(dataframe, asset_type=asset_type)
# Merge informative data (for 4h trend context)
if self.dp:
informative_btc = self.dp.get_pair_dataframe("BTC/USD", "4h")
informative_eth = self.dp.get_pair_dataframe("ETH/USD", "4h")
if informative_btc is not None and not informative_btc.empty:
informative_btc = self.populate_indicators_btc_4h(informative_btc, {})
dataframe = merge_informative_pair(
dataframe, informative_btc, self.timeframe, "4h", ffill=True
)
if informative_eth is not None and not informative_eth.empty:
informative_eth = self.populate_indicators_eth_4h(informative_eth, {})
dataframe = merge_informative_pair(
dataframe, informative_eth, self.timeframe, "4h", ffill=True
)
return dataframe
@informative("4h", "BTC/USD")
def populate_indicators_btc_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""4h BTC trend context"""
dataframe["ema_trend_4h"] = ta.EMA(dataframe, timeperiod=20) > ta.EMA(
dataframe, timeperiod=50
)
dataframe["rsi_4h"] = ta.RSI(dataframe, timeperiod=14)
macd_4h = ta.MACD(dataframe)
dataframe["trend_strength_4h"] = macd_4h["macd"] > macd_4h["macdsignal"]
return dataframe
@informative("4h", "ETH/USD")
def populate_indicators_eth_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""4h ETH trend context"""
dataframe["ema_trend_4h"] = ta.EMA(dataframe, timeperiod=20) > ta.EMA(
dataframe, timeperiod=50
)
dataframe["rsi_4h"] = ta.RSI(dataframe, timeperiod=14)
macd_4h = ta.MACD(dataframe)
dataframe["trend_strength_4h"] = macd_4h["macd"] > macd_4h["macdsignal"]
dataframe["volume_trend_4h"] = dataframe["volume"] > ta.SMA(
dataframe["volume"], timeperiod=10
)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Perform dual-asset selection and generate entry signal for the best pair.
"""
# Initialize entry column
dataframe["enter_long"] = 0
# The dataprovider is required for multi-asset analysis
if not self.dp:
return dataframe
# We only need to run the selection logic on one of the pairs.
# Arbitrarily choose BTC to be the "main" one to avoid duplicate processing.
if metadata["pair"] != "BTC/USD":
return dataframe
# Get the latest data for both assets
btc_df = self.dp.get_pair_dataframe("BTC/USD", self.timeframe)
eth_df = self.dp.get_pair_dataframe("ETH/USD", self.timeframe)
# Ensure we have enough data to analyze
if (
btc_df.empty
or len(btc_df) < self.startup_candle_count
or eth_df.empty
or len(eth_df) < self.startup_candle_count
):
return dataframe
# Run the pair selection logic
selection_result = self.pair_selector.select_best_pair(btc_df, eth_df)
self.latest_selection_result = selection_result # Cache for other methods
# If a pair was selected, find its corresponding dataframe and set the signal
if selection_result and selection_result["selected_pair"]:
selected_pair_name = selection_result["selected_pair"]["pair"]
if selected_pair_name == "BTC/USD":
# Set enter_long on the last candle of the BTC dataframe
btc_df.loc[btc_df.index[-1], "enter_long"] = 1
elif selected_pair_name == "ETH/USD":
# Set enter_long on the last candle of the ETH dataframe
# Note: We are modifying a different dataframe here. Freqtrade will process it.
eth_df.loc[eth_df.index[-1], "enter_long"] = 1
# Return the original dataframe. If BTC was selected, it will have the entry signal.
# If ETH was selected, its dataframe was modified in place.
return btc_df if metadata["pair"] == "BTC/USD" else eth_df
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Dynamic exit logic based on selected strategy type
"""
pair = metadata["pair"]
# Initialize exit signal
dataframe["exit_long"] = 0
if "BTC" in pair:
dataframe = self._populate_btc_exit(dataframe)
elif "ETH" in pair:
dataframe = self._populate_eth_exit(dataframe)
return dataframe
def _populate_btc_exit(self, dataframe: DataFrame) -> DataFrame:
"""BTC swing trading exit conditions"""
conditions = []
# RSI overbought
conditions.append(dataframe["rsi"] > 65)
# MACD bearish
conditions.append(
(dataframe["macd"] < dataframe["macdsignal"]) | (dataframe["macdhist"] < 0)
)
# Volume declining
conditions.append(dataframe["volume_ratio"] < 0.8)
# Bollinger Band upper region
conditions.append(dataframe["bb_percent"] > 0.8)
if conditions:
dataframe.loc[reduce(lambda x, y: x | y, conditions), "exit_long"] = 1
return dataframe
def _populate_eth_exit(self, dataframe: DataFrame) -> DataFrame:
"""ETH momentum trading exit conditions"""
conditions = []
# RSI extremely overbought
conditions.append(dataframe["rsi"] > 75)
# MACD momentum declining
conditions.append(
(dataframe["macd"] < dataframe["macdsignal"])
| (dataframe["macdhist"] < dataframe["macdhist"].shift(1))
)
# Volume declining
conditions.append(dataframe["volume_ratio"] < 0.7)
# Overextended above BB
conditions.append(dataframe["bb_percent"] > 1.1)
# Momentum weakening
conditions.append(~dataframe["momentum_positive"])
if conditions:
dataframe.loc[reduce(lambda x, y: x | y, conditions), "exit_long"] = 1
return dataframe
def custom_stoploss(
self,
pair: str,
trade: "Trade",
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> float:
"""
Dynamic stoploss based on pair volatility
"""
if "BTC" in pair:
return -0.025 # 2.5% for BTC (lower volatility)
elif "ETH" in pair:
return -0.030 # 3.0% for ETH (higher volatility)
else:
return self.stoploss
def custom_sell(
self,
pair: str,
trade: "Trade",
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> Optional[Union[str, bool]]:
"""
Dynamic profit-taking based on asset and strategy type
"""
if "BTC" in pair:
# BTC swing trading targets
if current_profit >= 0.08:
return "btc_profit_8pct"
elif current_profit >= 0.06:
return "btc_profit_6pct"
elif current_profit >= 0.04:
return None # Let exit signals handle
elif "ETH" in pair:
# ETH momentum trading targets
if current_profit >= 0.09:
return "eth_profit_9pct"
elif current_profit >= 0.07:
return "eth_profit_7pct"
elif current_profit >= 0.05:
return None # Let exit signals handle
return None
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:
"""
Confirm the trade and record it for allocation tracking.
"""
# Record the trade for allocation tracking
if (
self.latest_selection_result
and self.latest_selection_result["selected_pair"]
):
selected_pair_info = self.latest_selection_result["selected_pair"]
if selected_pair_info["pair"] == pair:
self.pair_selector.record_trade(
pair=pair, score=selected_pair_info["total_score"]
)
print(f"Trade confirmed for {pair}. Recorded for allocation tracking.")
return True
print(f"Trade confirmation failed for {pair}. No valid selection found.")
return False
def leverage(
self,
pair: str,
current_time: datetime,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
entry_tag: Optional[str],
side: str,
**kwargs,
) -> float:
"""
No leverage for Kraken spot trading
"""
return 1.0