Timeframe
5m
Direction
Long Only
Stoploss
-2.0%
Trailing Stop
No
ROI
0m: 8.0%, 60m: 4.0%, 120m: 2.0%, 240m: 0.0%
Interface Version
N/A
Startup Candles
200
Indicators
9
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
#!/usr/bin/env python3
"""
Fixed version of PerpSpotBasisStrategy
"""
import pandas as pd
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy import DecimalParameter
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
import os
class PerpSpotBasisStrategy_Fixed(IStrategy):
timeframe = "5m"
can_short = False
startup_candle_count = 200
use_exit_signal = False
# More reasonable ROI
minimal_roi = {
"0": 0.08,
"60": 0.04,
"120": 0.02,
"240": 0
}
stoploss = -0.02 # Using config stoploss
process_only_new_candles = True
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata['pair']
# Try to load futures data, but don't fail if it's not available
futures_data = self.load_futures_data(pair)
if futures_data is not None:
dataframe = self.merge_futures_data(dataframe, futures_data)
dataframe = self.calculate_basis_features(dataframe)
# Always calculate these basic features
dataframe = self.calculate_volume_features(dataframe)
# Technical indicators
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20)
dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# Bollinger Bands for additional signals
bb = ta.BBANDS(dataframe)
dataframe['bb_upper'] = bb['upperband']
dataframe['bb_middle'] = bb['middleband']
dataframe['bb_lower'] = bb['lowerband']
return dataframe
def load_futures_data(self, pair):
"""Load corresponding futures data if available"""
try:
futures_pair = pair.replace('/', '_') + '_USDT-5m-futures.feather'
futures_path = f'/home/gil/freqtrade/user_data/data/binance/futures/{futures_pair}'
if not os.path.exists(futures_path):
return None
df = pd.read_feather(futures_path)
return df
except Exception:
return None
def merge_futures_data(self, spot_df, futures_df):
"""Merge spot and futures data"""
try:
merged = pd.merge(spot_df, futures_df[['date', 'close', 'volume']],
left_on='date', right_on='date', how='left',
suffixes=('', '_perp'))
merged[['close_perp', 'volume_perp']] = merged[['close_perp', 'volume_perp']].ffill()
return merged
except Exception:
return spot_df
def calculate_basis_features(self, dataframe: DataFrame) -> DataFrame:
"""Calculate perp-spot basis features if perp data is available"""
if 'close_perp' not in dataframe.columns or dataframe['close_perp'].isna().all():
return dataframe
# Basic basis calculation
dataframe['basis'] = (dataframe['close_perp'] - dataframe['close']) / dataframe['close'] * 100
# Only calculate if we have valid data
if not dataframe['basis'].isna().all():
dataframe['basis_ma_10'] = ta.SMA(dataframe['basis'], timeperiod=10)
dataframe['basis_ma_30'] = ta.SMA(dataframe['basis'], timeperiod=30)
dataframe['basis_roc'] = ta.ROC(dataframe['basis'], timeperiod=5)
# Z-score of basis
basis_mean = dataframe['basis'].rolling(window=50).mean()
basis_std = dataframe['basis'].rolling(window=50).std()
dataframe['basis_zscore'] = (dataframe['basis'] - basis_mean) / basis_std
return dataframe
def calculate_volume_features(self, dataframe):
"""Calculate volume features"""
try:
dataframe['volume_ma_20'] = ta.SMA(dataframe['volume'], timeperiod=20)
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_ma_20']
dataframe['obv'] = ta.OBV(dataframe['close'], dataframe['volume'])
# VWAP
vwap_period = 20
cumulative_price_volume = (dataframe['close'] * dataframe['volume']).rolling(vwap_period).sum()
cumulative_volume = dataframe['volume'].rolling(vwap_period).sum()
dataframe['vwap'] = cumulative_price_volume / cumulative_volume
return dataframe
except Exception:
return dataframe
freqai_prediction_threshold = DecimalParameter(0.6, 0.8, default=0.7, space='buy', optimize=True, load=True)
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, 'enter_long'] = 0
# Primary: FreqAI signal
if '&-enter_long' in dataframe.columns:
freqai_condition = dataframe['&-enter_long'] > self.freqai_prediction_threshold.value
dataframe.loc[freqai_condition, 'enter_long'] = 1
else:
# Fallback: Conservative technical analysis
oversold = dataframe['rsi'] < 30
uptrend = dataframe['ema_20'] > dataframe['ema_50']
macd_bullish = dataframe['macd'] > dataframe['macdsignal']
near_bb_lower = dataframe['close'] <= dataframe['bb_lower'] * 1.02 # Within 2% of BB lower
volume_confirmation = dataframe['volume_ratio'] > 1.1
# Require multiple confirmations
entry_condition = oversold & uptrend & macd_bullish & near_bb_lower & volume_confirmation
# Additional filter: only enter if price is above VWAP (trend confirmation)
price_above_vwap = dataframe['close'] > dataframe['vwap']
entry_condition = entry_condition & price_above_vwap
dataframe.loc[entry_condition, 'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, 'exit_long'] = 0
# Exit conditions
overbought = dataframe['rsi'] > 75
near_bb_upper = dataframe['close'] >= dataframe['bb_upper'] * 0.98
# Exit if basis normalizes (only if we have basis data)
basis_exit = False
if 'basis_zscore' in dataframe.columns and not dataframe['basis_zscore'].isna().all():
basis_exit = dataframe['basis_zscore'] > 1.5
exit_condition = overbought | near_bb_upper | basis_exit
dataframe.loc[exit_condition, 'exit_long'] = 1
return dataframe
# FreqAI configuration - robust feature set
freqai_info = {
"identifier": "SKLearnRandomForestClassifier",
"features": [
"rsi",
"ema_20",
"ema_50",
"adx",
"macd",
"macdsignal",
"macdhist",
"volume_ratio",
"obv",
"vwap",
"bb_upper",
"bb_middle",
"bb_lower",
# Include basis features if available, FreqAI will handle missing values
"basis_ma_10",
"basis_ma_30",
"basis_zscore",
],
"label_period_candles": 24, # Predict 2 hours ahead
"data_split_parameters": {"split_train": 0.8, "split_test": 0.2},
"model_training_parameters": {
"n_estimators": 100,
"max_depth": 8,
"min_samples_split": 10,
"min_samples_leaf": 5,
"random_state": 42,
"n_jobs": -1
}
}