hyperopt ok

This commit is contained in:
zhangkun9038@dingtalk.com 2025-05-15 13:08:38 +00:00
parent 894b6c5f50
commit b1c6720c93
31 changed files with 296 additions and 5319 deletions

View File

@ -19,6 +19,8 @@
"order_book_top": 1
},
"stake_currency": "USDT",
"trailing_stop": false,
"use_custom_stoploss": true,
"startup_candle_count": 200,
"tradable_balance_ratio": 1,
"fiat_display_currency": "USD",
@ -27,7 +29,23 @@
"timeframe": "5m",
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": true,
"stoploss": -0.1,
"stoploss": -0.061,
"roi": {},
"trailing": {
"trailing_stop": false,
"trailing_stop_positive": null,
"trailing_stop_positive_offset": 0.0,
"trailing_only_offset_is_reached": false
},
"max_open_trades": 3,
"buy": {
"adx_buy": 25,
"atr_ratio": 0.005
},
"sell": {
"ema_fast_period": 7,
"rsi_sell": 60
},
"exchange": {
"name": "okx",
"key": "REDACTED",
@ -45,7 +63,7 @@
"rateLimit": 3000,
"timeout": 20000
},
"pair_whitelist": ["OKB/USDT", "BTC/USDT", "ETH/USDT", "SOL/USDT", "DOT/USDT", "TON/USDT"],
"pair_whitelist": ["BTC/USDT", "ETH/USDT", "SOL/USDT", "DOT/USDT"],
"pair_blacklist": []
},
"pairlists": [

BIN
doc/.hyperopts.md.swp Normal file

Binary file not shown.

14
doc/hyperopts.md Normal file
View File

@ -0,0 +1,14 @@
## hyperopts如何使用
每周运行一次 hyperopt, 每次运行最近2个月的数据, 得到 stoploss 最优解后 手动更新 config, 提交代码
假设今天是2025年5月15日
```
cd tools
./download.sh
./hyperopt.sh 20250314 20250516
然后得到结果, stoploss 为 -0.06 写到log里
```
后面改成自动更新并不麻烦
策略目录有自动生成的json, 是hypertop计算结果, 需脱掉一层 paramas后 方可使用

View File

@ -1,59 +0,0 @@
from freqtrade.strategy.interface import IStrategy
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from pandas import DataFrame
import numpy as np
import logging
logger = logging.getLogger(__name__)
class AIEnhancedStrategy(IStrategy):
INTERFACE_VERSION = 3
can_short = False # 只做多
timeframe = '5m'
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: dict, **kwargs) -> DataFrame:
# 自定义特征工程
for col in ['open', 'high', 'low', 'close', 'volume']:
dataframe[f'{col}_pct_change'] = dataframe[col].pct_change(periods=period)
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
# 目标变量未来n根K线的收盘价变化
dataframe['&s-close_pct'] = dataframe['close'].pct_change(periods=5).shift(-5)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dk = self.freqai_info.get("dk", None)
if not dk:
raise ValueError("FreqaiDataKitchen is not available.")
dataframe = dk.feature_engineering_standard(dataframe, metadata, self)
if self.config["runmode"].value in ("live", "dry_run"):
dataframe = dk.live_models(self, dataframe, metadata=metadata)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 预测值大于阈值时开多仓
dataframe.loc[
(dataframe["&s-close_pct"] > 0.002),
"enter_long"
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 预测值小于负阈值时平仓
dataframe.loc[
(dataframe["&s-close_pct"] < -0.001),
"exit_long"
] = 1
return dataframe

View File

@ -1,190 +0,0 @@
from freqtrade.strategy import IStrategy, IntParameter, RealParameter, DecimalParameter, BooleanParameter
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from datetime import datetime
class BsseHyperOptStrategy(IStrategy):
# 策略的基本配置
timeframe = '5m'
minimal_roi = {"40": 0.0, "30": 0.01, "20": 0.02, "0": 0.04}
stoploss = -0.10
startup_candle_count = 20
# 可选的订单类型映射
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "limit",
"stoploss_on_exchange": False,
}
# 可选的订单有效时间
order_time_in_force = {
"entry": "gtc",
"exit": "gtc",
}
# 可优化的参数
buy_rsi = IntParameter(30, 50, default=35, space='buy')
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
sell_minusdi = DecimalParameter(
low=0, high=1, default=0.5001, decimals=3, space='sell', load=False
)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
@property
def protections(self):
prot = []
if self.protection_enabled.value:
prot.append(
{
"method": "CooldownPeriod",
"stop_duration_candles": self.protection_cooldown_lookback.value,
}
)
return prot
def bot_start(self):
pass
def informative_pairs(self):
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 动量指标
# ADX
dataframe["adx"] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
dataframe["macdhist"] = macd["macdhist"]
# 负向指标
dataframe["minus_di"] = ta.MINUS_DI(dataframe)
# 正向指标
dataframe["plus_di"] = ta.PLUS_DI(dataframe)
# RSI
dataframe["rsi"] = ta.RSI(dataframe)
# 快速随机指标
stoch_fast = ta.STOCHF(dataframe)
dataframe["fastd"] = stoch_fast["fastd"]
dataframe["fastk"] = stoch_fast["fastk"]
# 布林带
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["ema10"] = ta.EMA(dataframe, timeperiod=10)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["rsi"] < self.buy_rsi.value)
& (dataframe["fastd"] < 35)
& (dataframe["adx"] > 30)
& (dataframe["plus_di"] > self.buy_plusdi.value)
)
| ((dataframe["adx"] > 65) & (dataframe["plus_di"] > self.buy_plusdi.value)),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe["rsi"], self.sell_rsi.value))
| (qtpylib.crossed_above(dataframe["fastd"], 70))
)
& (dataframe["adx"] > 10)
& (dataframe["minus_di"] > 0)
)
| ((dataframe["adx"] > 70) & (dataframe["minus_di"] > self.sell_minusdi.value)),
"exit_long",
] = 1
return dataframe
def leverage(
self,
pair: str,
current_time: datetime,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
entry_tag: str | None,
side: str,
**kwargs,
) -> float:
return 3.0
def adjust_trade_position(
self,
trade,
current_time: datetime,
current_rate: float,
current_profit: float,
min_stake: float | None,
max_stake: float,
current_entry_rate: float,
current_exit_rate: float,
current_entry_profit: float,
current_exit_profit: float,
**kwargs,
) -> float | None:
if current_profit < -0.0075:
orders = trade.select_filled_orders(trade.entry_side)
return round(orders[0].stake_amount, 0)
return None
class HyperOpt:
def buy_indicator_space(self):
return [
self.strategy.buy_rsi.get_space('buy_rsi'),
self.strategy.buy_plusdi.get_space('buy_plusdi')
]
def sell_indicator_space(self):
return [
self.strategy.sell_rsi.get_space('sell_rsi'),
self.strategy.sell_minusdi.get_space('sell_minusdi')
]
def roi_space(self):
from skopt.space import Integer
return [
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
]
def stoploss_space(self):
from skopt.space import Real
return [Real(-0.5, -0.01, name='stoploss')]
def trailing_space(self):
from skopt.space import Real, Categorical
return [
Categorical([True, False], name='trailing_stop'),
Real(0.01, 0.1, name='trailing_stop_positive'),
Real(0.0, 0.1, name='trailing_stop_positive_offset'),
Real(0.0, 0.1, name='trailing_only_offset_is_reached')
]
def max_open_trades_space(self):
from skopt.space import Integer
return [Integer(1, 10, name='max_open_trades')]``

View File

@ -1,297 +0,0 @@
import logging
import numpy as np # noqa
import pandas as pd # noqa
import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib
from freqtrade.strategy import IntParameter, IStrategy, merge_informative_pair
logger = logging.getLogger(__name__)
class FreqaiExampleHybridStrategy(IStrategy):
"""
Example of a hybrid FreqAI strat, designed to illustrate how a user may employ
FreqAI to bolster a typical Freqtrade strategy.
Launching this strategy would be:
freqtrade trade --strategy FreqaiExampleHybridStrategy --strategy-path freqtrade/templates
--freqaimodel CatboostClassifier --config config_examples/config_freqai.example.json
or the user simply adds this to their config:
"freqai": {
"enabled": true,
"purge_old_models": 2,
"train_period_days": 15,
"identifier": "unique-id",
"feature_parameters": {
"include_timeframes": [
"3m",
"15m",
"1h"
],
"include_corr_pairlist": [
"BTC/USDT",
"ETH/USDT"
],
"label_period_candles": 20,
"include_shifted_candles": 2,
"DI_threshold": 0.9,
"weight_factor": 0.9,
"principal_component_analysis": false,
"use_SVM_to_remove_outliers": true,
"indicator_periods_candles": [10, 20]
},
"data_split_parameters": {
"test_size": 0,
"random_state": 1
},
"model_training_parameters": {
"n_estimators": 200,
"max_depth": 5,
"learning_rate": 0.05
}
},
Thanks to @smarmau and @johanvulgt for developing and sharing the strategy.
"""
minimal_roi = {
# "120": 0.0, # exit after 120 minutes at break even
"60": 0.01,
"30": 0.02,
"0": 0.04,
}
plot_config = {
"main_plot": {
"tema": {},
},
"subplots": {
"MACD": {
"macd": {"color": "blue"},
"macdsignal": {"color": "orange"},
},
"RSI": {
"rsi": {"color": "red"},
},
"Up_or_down": {
"&s-up_or_down": {"color": "green"},
},
},
}
process_only_new_candles = True
stoploss = -0.05
use_exit_signal = True
startup_candle_count: int = 30
can_short = False
# Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True)
def feature_engineering_expand_all(
self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
"""
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=period, stds=2
)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
dataframe["%-bb_width-period"] = (
dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]
) / dataframe["bb_middleband-period"]
return dataframe
def feature_engineering_expand_basic(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
return dataframe
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
"""
Redefined target variable to predict whether the price will increase or decrease in the future.
"""
logger.info(f"Setting FreqAI targets for pair: {metadata['pair']}")
if "close" not in dataframe.columns:
logger.error("Required 'close' column missing in dataframe")
raise ValueError("Required 'close' column missing in dataframe")
if len(dataframe) < 50:
logger.error(f"Insufficient data: {len(dataframe)} rows, need at least 50 for shift(-50)")
raise ValueError("Insufficient data for target calculation")
try:
# Define target variable: 1 for price increase, 0 for price decrease
dataframe["&-up_or_down"] = np.where(
dataframe["close"].shift(-50) > dataframe["close"], 1, 0
)
# Ensure target variable is a 2D array
dataframe["&-up_or_down"] = dataframe["&-up_or_down"].values.reshape(-1, 1)
except Exception as e:
logger.error(f"Failed to create &-up_or_down column: {str(e)}")
raise
logger.info("FreqAI targets set successfully")
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.info(f"Processing pair: {metadata['pair']}")
logger.info(f"Input DataFrame shape: {dataframe.shape}")
logger.info(f"Input DataFrame columns: {list(dataframe.columns)}")
logger.info(f"Input DataFrame head:\n{dataframe[['date', 'close', 'volume']].head().to_string()}")
# Ensure FreqAI processing
logger.info("Calling self.freqai.start")
try:
dataframe = self.freqai.start(dataframe, metadata, self)
except Exception as e:
logger.error(f"self.freqai.start failed: {str(e)}")
raise
logger.info("self.freqai.start completed")
logger.info(f"Output DataFrame shape: {dataframe.shape}")
logger.info(f"Output DataFrame columns: {list(dataframe.columns)}")
# Safely log columns that exist
available_columns = [col for col in ['date', 'close', '&-up_or_down'] if col in dataframe.columns]
logger.info(f"Output DataFrame head:\n{dataframe[available_columns].head().to_string()}")
if "&-up_or_down" not in dataframe.columns:
logger.error("FreqAI did not generate the required &-up_or_down column")
raise KeyError("FreqAI did not generate the required &-up_or_down column")
# RSI
dataframe["rsi"] = ta.RSI(dataframe)
# 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"]
)
dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe[
"bb_middleband"
]
# TEMA
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
df.loc[
(
(qtpylib.crossed_above(df["rsi"], self.buy_rsi.value))
& (df["tema"] <= df["bb_middleband"])
& (df["tema"] > df["tema"].shift(1))
& (df["volume"] > 0)
),
"enter_long",
] = 1
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
df.loc[
(
(qtpylib.crossed_above(df["rsi"], self.sell_rsi.value))
& (df["tema"] > df["bb_middleband"])
& (df["tema"] < df["tema"].shift(1))
& (df["volume"] > 0)
),
"exit_long",
] = 1
return df

View File

@ -1,32 +0,0 @@
{
"strategy_name": "FreqaiExampleStrategy",
"params": {
"trailing": {
"trailing_stop": true,
"trailing_stop_positive": 0.01,
"trailing_stop_positive_offset": 0.02,
"trailing_only_offset_is_reached": false
},
"max_open_trades": {
"max_open_trades": 4
},
"buy": {
"buy_rsi": 39.92672300850069
},
"sell": {
"sell_rsi": 69.92672300850067
},
"protection": {},
"roi": {
"0": 0.132,
"8": 0.047,
"14": 0.007,
"60": 0
},
"stoploss": {
"stoploss": -0.322
}
},
"ft_stratparam_v": 1,
"export_time": "2025-04-23 12:30:05.550433+00:00"
}

View File

@ -1,395 +0,0 @@
import logging
import numpy as np
import pandas as pd # 添加 pandas 导入
from functools import reduce
import talib.abstract as ta
from pandas import DataFrame
from typing import Dict, List, Optional
from technical import qtpylib
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
logger = logging.getLogger(__name__)
class FreqaiExampleStrategy(IStrategy):
# 移除硬编码的 minimal_roi 和 stoploss改为动态适配
minimal_roi = {} # 将在 populate_indicators 中动态生成
stoploss = 0.0 # 将在 populate_indicators 中动态设置
trailing_stop = True
process_only_new_candles = True
use_exit_signal = True
startup_candle_count: int = 40
can_short = False
# 参数定义FreqAI 动态适配 buy_rsi 和 sell_rsi禁用 Hyperopt 优化
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
# 为 Hyperopt 优化添加 ROI 和 stoploss 参数
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, load=True)
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.027, space="roi", optimize=True, load=True)
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.009, space="roi", optimize=True, load=True)
stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.182, space="stoploss", optimize=True, load=True)
# FreqAI 配置
freqai_info = {
"model": "XGBoostRegressor", # 与config保持一致
"save_backtest_models": True,
"feature_parameters": {
"include_timeframes": ["3m", "15m", "1h"], # 与config一致:w
"include_corr_pairlist": ["BTC/USDT", "SOL/USDT"], # 添加相关交易对
"label_period_candles": 20, # 与config一致
"include_shifted_candles": 2, # 与config一致
},
"data_split_parameters": {
"test_size": 0.2,
"shuffle": True, # 启用shuffle
},
"model_training_parameters": {
"n_estimators": 100, # 减少树的数量
"learning_rate": 0.1, # 提高学习率
"max_depth": 6, # 限制树深度
"subsample": 0.8, # 添加子采样
"colsample_bytree": 0.8, # 添加特征采样
"objective": "reg:squarederror",
"eval_metric": "rmse",
"early_stopping_rounds": 20,
"verbose": 0,
},
"data_kitchen": {
"feature_parameters": {
"DI_threshold": 1.5, # 降低异常值过滤阈值
"use_DBSCAN_to_remove_outliers": False # 禁用DBSCAN
}
}
}
plot_config = {
"main_plot": {},
"subplots": {
"&-buy_rsi": {"&-buy_rsi": {"color": "green"}},
"&-sell_rsi": {"&-sell_rsi": {"color": "red"}},
"&-stoploss": {"&-stoploss": {"color": "purple"}},
"&-roi_0": {"&-roi_0": {"color": "orange"}},
"do_predict": {"do_predict": {"color": "brown"}},
},
}
def __init__(self, config: Dict):
super().__init__(config)
# 初始化特征缓存
self.feature_cache = {}
# 设置日志级别
logger.setLevel(logging.DEBUG)
# 输出模型路径用于调试
freqai_model_path = self.config.get("freqai", {}).get("model_path", "/freqtrade/user_data/models")
logger.info(f"FreqAI 模型路径:{freqai_model_path}")
def _normalize_column(self, series: pd.Series) -> pd.Series:
"""对单个列进行最小最大归一化"""
if series.nunique() <= 1:
# 如果列中所有值都相同或为空直接返回全0
return pd.Series(np.zeros_like(series), index=series.index)
min_val = series.min()
max_val = series.max()
normalized = (series - min_val) / (max_val - min_val)
return normalized.fillna(0)
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
# 基础指标
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["macd"], dataframe["macdsignal"], dataframe["macdhist"] = ta.MACD(
dataframe, fastperiod=12, slowperiod=26, signalperiod=9
)
# 布林带及其宽度
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"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"]
# ATR (Average True Range)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
# RSI 变化率
dataframe["rsi_gradient"] = dataframe["rsi"].diff().fillna(0)
# 数据清理与归一化
for col in dataframe.select_dtypes(include=[np.number]).columns:
# Ensure column is valid and contains more than one unique value to avoid division by zero
if dataframe[col].nunique() > 1:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill().fillna(0)
dataframe[f"{col}_norm"] = self._normalize_column(dataframe[col])
else:
dataframe[f"{col}_norm"] = 0 # Default if normalization not possible
logger.info(f"特征工程完成,特征数量:{len(dataframe.columns)}")
return dataframe
def _add_noise(self, dataframe: DataFrame, noise_level: float = 0.02) -> DataFrame:
"""为数值型特征添加随机噪声以增强模型泛化能力"""
df = dataframe.copy()
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
for col in numeric_cols:
noise = np.random.normal(loc=0.0, scale=noise_level * df[col].std(), size=df.shape[0])
df[col] += noise
logger.info(f"已向 {len(numeric_cols)} 个数值型特征添加 {noise_level * 100:.0f}% 噪声")
return df
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
dataframe["%-volume_change"] = dataframe["volume"].pct_change(periods=5)
dataframe["%-price_momentum"] = dataframe["close"] / dataframe["close"].shift(20) - 1
# 数据清理逻辑
for col in dataframe.columns:
if dataframe[col].dtype in ["float64", "int64"]:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
dataframe[col] = dataframe[col].ffill()
dataframe[col] = dataframe[col].fillna(0)
# 检查是否仍有无效值
if dataframe[col].isna().any() or np.isinf(dataframe[col]).any():
logger.warning(f"{col} 仍包含无效值,已填充为默认值")
dataframe[col] = dataframe[col].fillna(0)
# 添加 2% 噪声以增强模型鲁棒性
dataframe = self._add_noise(dataframe, noise_level=0.02)
logger.info(f"特征工程完成,特征数量:{len(dataframe.columns)}")
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
dataframe.replace([np.inf, -np.inf], 0, inplace=True)
dataframe.ffill(inplace=True)
dataframe.fillna(0, inplace=True)
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
if "close" not in dataframe.columns:
logger.error("数据框缺少必要的 'close'")
raise ValueError("数据框缺少必要的 'close'")
try:
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
# 定义目标变量为未来价格变化百分比(连续值)
dataframe["up_or_down"] = (
dataframe["close"].shift(-label_period) - dataframe["close"]
) / dataframe["close"]
# 数据清理:处理 NaN 和 Inf 值
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
# 确保目标变量是二维数组
if dataframe["up_or_down"].ndim == 1:
dataframe["up_or_down"] = dataframe["up_or_down"].values.reshape(-1, 1)
# 检查并处理 NaN 或无限值
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
# 生成 %-volatility 特征
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
# 确保 &-buy_rsi_pred 列的值计算正确
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
if "&-buy_rsi_pred" not in dataframe.columns:
logger.warning("&-buy_rsi_pred 列不存在,正在使用 &-buy_rsi 模拟替代")
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(window=10).mean().clip(20, 40)
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi_pred"].fillna(dataframe["&-buy_rsi_pred"].median())
# 确保 &-sell_rsi_pred 存在(基于 buy_rsi_pred + 偏移量)
if "&-sell_rsi_pred" not in dataframe.columns:
logger.warning("&-sell_rsi_pred 列不存在,正在使用 &-buy_rsi_pred + 20 模拟替代")
dataframe["&-sell_rsi_pred"] = dataframe["&-buy_rsi_pred"] + 20
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].clip(50, 90)
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].fillna(dataframe["&-sell_rsi_pred"].median())
# 数据清理
for col in ["&-buy_rsi", "up_or_down", "%-volatility"]:
# 使用直接操作避免链式赋值
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill() # 替代 fillna(method='ffill')
dataframe[col] = dataframe[col].fillna(dataframe[col].mean()) # 使用均值填充 NaN 值
if dataframe[col].isna().any():
logger.warning(f"目标列 {col} 仍包含 NaN填充为默认值")
except Exception as e:
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
raise
# Log the shape of the target variable for debugging
logger.info(f"目标列形状:{dataframe['up_or_down'].shape}")
logger.info(f"目标列预览:\n{dataframe[['up_or_down', '&-buy_rsi']].head().to_string()}")
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.info(f"处理交易对:{metadata['pair']}")
dataframe = self.freqai.start(dataframe, metadata, self)
# 计算传统指标
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
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["tema"] = ta.TEMA(dataframe, timeperiod=9)
# 生成 up_or_down 信号(非 FreqAI 目标)
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["up_or_down"] = np.where(
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
)
# 确保 do_predict 列存在并填充默认值
if "do_predict" not in dataframe.columns:
dataframe["do_predict"] = 0
logger.warning("do_predict 列不存在,已创建并填充为 0。")
# 添加详细日志以验证 do_predict 的值
if "do_predict" in dataframe.columns:
logger.debug(f"do_predict 列存在前5行预览\n{dataframe[['do_predict']].head().to_string()}")
else:
logger.warning("do_predict 列不存在,可能未正确加载模型或进行预测。")
# 确保 &-buy_rsi_pred 存在(如果模型未提供则模拟生成)
if "&-buy_rsi" in dataframe.columns:
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(window=10).mean().clip(20, 40)
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi_pred"].fillna(dataframe["&-buy_rsi_pred"].median())
else:
logger.warning("&-buy_rsi 列不存在,无法生成 &-buy_rsi_pred将使用默认值")
dataframe["&-buy_rsi_pred"] = 27 # 默认 RSI 买入阈值
# 生成 &-sell_rsi_pred基于 buy_rsi_pred + 偏移量)
if "&-buy_rsi_pred" in dataframe.columns:
dataframe["&-sell_rsi_pred"] = dataframe["&-buy_rsi_pred"] + 20
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].clip(50, 90)
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].fillna(dataframe["&-sell_rsi_pred"].median())
else:
logger.warning("&-buy_rsi_pred 列不存在,无法生成 &-sell_rsi_pred将使用默认值")
dataframe["&-sell_rsi_pred"] = 59 # 默认 RSI 卖出阈值
# 确保 &-sell_rsi_pred 最终存在于 dataframe 中
if "&-sell_rsi_pred" not in dataframe.columns:
logger.error("&-sell_rsi_pred 列仍然缺失,策略可能无法正常运行")
raise ValueError("&-sell_rsi_pred 列缺失,无法继续执行策略逻辑")
# 生成 stoploss_pred基于波动率
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
dataframe["&-stoploss"] = (-0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)).fillna(-0.1)
dataframe["&-stoploss"] = dataframe["&-stoploss"].clip(-0.35, -0.1)
# 生成 roi_0_pred基于 ROI 参数)
dataframe["&-roi_0"] = ((dataframe["close"] / dataframe["close"].shift(label_period) - 1).clip(0, 0.2)).fillna(0)
# 设置策略级参数
try:
self.buy_rsi.value = float(dataframe["&-buy_rsi_pred"].iloc[-1])
self.sell_rsi.value = float(dataframe["&-sell_rsi_pred"].iloc[-1])
except Exception as e:
logger.error(f"设置 buy_rsi/sell_rsi 失败:{str(e)}")
self.buy_rsi.value = 27
self.sell_rsi.value = 59
self.stoploss = -0.15 # 固定止损 15%
self.minimal_roi = {
0: float(self.roi_0.value),
15: float(self.roi_15.value),
30: float(self.roi_30.value),
60: 0
}
# 更保守的追踪止损设置
self.trailing_stop_positive = 0.05 # 追踪止损触发点
self.trailing_stop_positive_offset = 0.1 # 追踪止损偏移量
logger.info(f"动态参数buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, "
f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}")
# 数据清理
dataframe.replace([np.inf, -np.inf], 0, inplace=True)
dataframe.ffill(inplace=True)
dataframe.fillna(0, inplace=True)
return dataframe
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
# 改进卖出信号条件
exit_long_conditions = [
(df["rsi"] > df["&-sell_rsi_pred"]), # RSI 高于卖出阈值
(df["volume"] > df["volume"].rolling(window=10).mean()), # 成交量高于近期均值
(df["close"] < df["bb_middleband"]) # 价格低于布林带中轨
]
# 添加详细日志以验证 do_predict 的值
if "do_predict" in df.columns:
logger.debug(f"do_predict 列存在前5行预览\n{df[['do_predict']].head().to_string()}")
else:
logger.warning("do_predict 列不存在,可能未正确加载模型或进行预测。")
if exit_long_conditions:
df.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
"exit_long"
] = 1
if "&-buy_rsi_pred" in df.columns:
logger.debug(f"&-buy_rsi_pred 列存在前5行预览\n{df[['&-buy_rsi_pred']].head().to_string()}")
else:
logger.warning("&-buy_rsi_pred 列不存在,可能未正确生成或被覆盖。")
return df
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
# 改进买入信号条件
# 检查 MACD 列是否存在
if "macd" not in df.columns or "macdsignal" not in df.columns:
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号。尝试重新计算 MACD 列。")
try:
macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
df["macd"] = macd["macd"]
df["macdsignal"] = macd["macdsignal"]
logger.info("MACD 列已成功重新计算。")
except Exception as e:
logger.error(f"重新计算 MACD 列时出错:{str(e)}")
raise ValueError("DataFrame 缺少必要的 MACD 列且无法重新计算。")
enter_long_conditions = [
(df["rsi"] < df["&-buy_rsi_pred"]), # RSI 低于买入阈值
(df["volume"] > df["volume"].rolling(window=10).mean() * 1.2), # 成交量高于近期均值20%
(df["close"] > df["bb_middleband"]) # 价格高于布林带中轨
]
# 如果 MACD 列存在,则添加 MACD 金叉条件
if "macd" in df.columns and "macdsignal" in df.columns:
enter_long_conditions.append((df["macd"] > df["macdsignal"]))
# 确保模型预测为买入
enter_long_conditions.append((df["do_predict"] == 1))
# 添加详细日志以验证 do_predict 的值
if "do_predict" in df.columns:
logger.debug(f"do_predict 列存在前5行预览\n{df[['do_predict']].head().to_string()}")
else:
logger.warning("do_predict 列不存在,可能未正确加载模型或进行预测。")
if enter_long_conditions:
df.loc[
reduce(lambda x, y: x & y, enter_long_conditions),
["enter_long", "enter_tag"]
] = (1, "long")
return df
def confirm_trade_entry(
self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time, entry_tag, side: str, **kwargs
) -> bool:
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = df.iloc[-1].squeeze()
if side == "long":
if rate > (last_candle["close"] * (1 + 0.0025)):
return False
return True

View File

@ -1,247 +0,0 @@
import logging
import numpy as np
from functools import reduce
import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
logger = logging.getLogger(__name__)
class MLBasedSentimentStrategy(IStrategy):
# 参数定义MLBasedSentimentStrategy 动态适配 buy_rsi 和 sell_rsi禁用 Hyperopt 优化
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
# 市场情绪参数
sentiment_weight = DecimalParameter(low=0.1, high=0.9, default=0.5, space="buy", optimize=True, load=True)
# ROI 参数
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.05, space="buy", optimize=True, load=True)
roi_15 = DecimalParameter(low=0.01, high=0.15, default=0.03, space="buy", optimize=True, load=True)
roi_30 = DecimalParameter(low=0.01, high=0.1, default=0.02, space="buy", optimize=True, load=True)
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
# 保留关键的技术指标
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
# 确保 MACD 列被正确计算并保留
try:
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
except Exception as e:
logger.error(f"计算 MACD 列时出错:{str(e)}")
dataframe["macd"] = np.nan
dataframe["macdsignal"] = np.nan
# 检查 MACD 列是否存在
if "macd" not in dataframe.columns or "macdsignal" not in dataframe.columns:
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号")
raise ValueError("DataFrame 缺少必要的 MACD 列")
# 确保 MACD 列存在
if "macd" not in dataframe.columns or "macdsignal" not in dataframe.columns:
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号")
raise ValueError("DataFrame 缺少必要的 MACD 列")
# 保留布林带相关特征
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["volume_ma"] = dataframe["volume"].rolling(window=20).mean()
# 添加市场情绪特征
# 假设我们有一个外部数据源提供市场情绪分数
# 这里我们使用一个示例值,实际应用中需要从外部数据源获取
dataframe["sentiment_score"] = 0.5 # 示例值,实际应替换为真实数据
# 数据清理
for col in dataframe.columns:
if dataframe[col].dtype in ["float64", "int64"]:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill().fillna(0)
logger.info(f"特征工程完成,特征数量:{len(dataframe.columns)}")
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
if "close" not in dataframe.columns:
logger.error("数据框缺少必要的 'close'")
raise ValueError("数据框缺少必要的 'close'")
try:
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
# 定义目标变量为未来价格变化百分比(连续值)
dataframe["up_or_down"] = (
dataframe["close"].shift(-label_period) - dataframe["close"]
) / dataframe["close"]
# 数据清理:处理 NaN 和 Inf 值
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
# 确保目标变量是二维数组
if dataframe["up_or_down"].ndim == 1:
dataframe["up_or_down"] = dataframe["up_or_down"].values.reshape(-1, 1)
# 检查并处理 NaN 或无限值
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
# 生成 %-volatility 特征
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
# 确保 &-buy_rsi 列的值计算正确
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
# 数据清理
for col in ["&-buy_rsi", "up_or_down", "%-volatility"]:
# 使用直接操作避免链式赋值
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill() # 替代 fillna(method='ffill')
dataframe[col] = dataframe[col].fillna(dataframe[col].mean()) # 使用均值填充 NaN 值
if dataframe[col].isna().any():
logger.warning(f"目标列 {col} 仍包含 NaN填充为默认值")
except Exception as e:
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
raise
# Log the shape of the target variable for debugging
logger.info(f"目标列形状:{dataframe['up_or_down'].shape}")
logger.info(f"目标列预览:\n{dataframe[['up_or_down', '&-buy_rsi']].head().to_string()}")
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.info(f"处理交易对:{metadata['pair']}")
dataframe = self.freqai.start(dataframe, metadata, self)
# 计算传统指标
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
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["tema"] = ta.TEMA(dataframe, timeperiod=9)
# 生成 up_or_down 信号(非 FreqAI 目标)
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
# 使用未来价格变化方向生成 up_or_down 信号
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["up_or_down"] = np.where(
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
)
# 动态设置参数
if "&-buy_rsi" in dataframe.columns:
# 派生其他目标
dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
# Ensure proper calculation and handle potential NaN values
dataframe["&-stoploss"] = (-0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)).fillna(-0.1)
dataframe["&-roi_0"] = ((dataframe["close"] / dataframe["close"].shift(label_period) - 1).clip(0, 0.2)).fillna(0)
# Additional check to ensure no NaN values remain
for col in ["&-stoploss", "&-roi_0"]:
if dataframe[col].isna().any():
logger.warning(f"{col} 仍包含 NaN填充为默认值")
dataframe[col] = dataframe[col].fillna(-0.1 if col == "&-stoploss" else 0)
# 简化动态参数生成逻辑
# 放松 buy_rsi 和 sell_rsi 的生成逻辑
# 计算 buy_rsi_pred 并清理 NaN 值
dataframe["buy_rsi_pred"] = dataframe["rsi"].rolling(window=10).mean().clip(30, 50)
dataframe["buy_rsi_pred"] = dataframe["buy_rsi_pred"].fillna(dataframe["buy_rsi_pred"].median())
# 计算 sell_rsi_pred 并清理 NaN 值
dataframe["sell_rsi_pred"] = dataframe["buy_rsi_pred"] + 20
dataframe["sell_rsi_pred"] = dataframe["sell_rsi_pred"].fillna(dataframe["sell_rsi_pred"].median())
# 计算 stoploss_pred 并清理 NaN 值
dataframe["stoploss_pred"] = -0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)
dataframe["stoploss_pred"] = dataframe["stoploss_pred"].fillna(dataframe["stoploss_pred"].mean())
# 计算 roi_0_pred 并清理 NaN 值
dataframe["roi_0_pred"] = dataframe["&-roi_0"].clip(0.01, 0.2)
dataframe["roi_0_pred"] = dataframe["roi_0_pred"].fillna(dataframe["roi_0_pred"].mean())
# 检查预测值
for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred", "&-sell_rsi", "&-stoploss", "&-roi_0"]:
if dataframe[col].isna().any():
logger.warning(f"{col} 包含 NaN填充为默认值")
dataframe[col] = dataframe[col].fillna(dataframe[col].mean())
# 更保守的止损和止盈设置
dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.3).clip(0.01, 0.2)
dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3)
# 设置策略级参数
self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1])
self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1])
# 更保守的止损设置
self.stoploss = -0.15 # 固定止损 15%
self.minimal_roi = {
0: float(self.roi_0.value),
15: float(self.roi_15.value),
30: float(self.roi_30.value),
60: 0
}
# 更保守的追踪止损设置
self.trailing_stop_positive = 0.05 # 追踪止损触发点
self.trailing_stop_positive_offset = 0.1 # 追踪止损偏移量
logger.info(f"动态参数buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, "
f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}")
dataframe.replace([np.inf, -np.inf], 0, inplace=True)
dataframe.ffill(inplace=True)
dataframe.fillna(0, inplace=True)
logger.info(f"up_or_down 值统计:\n{dataframe['up_or_down'].value_counts().to_string()}")
logger.info(f"do_predict 值统计:\n{dataframe['do_predict'].value_counts().to_string()}")
return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
# 改进买入信号条件
# 检查 MACD 列是否存在
if "macd" not in df.columns or "macdsignal" not in df.columns:
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号。尝试重新计算 MACD 列。")
try:
macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
df["macd"] = macd["macd"]
df["macdsignal"] = macd["macdsignal"]
logger.info("MACD 列已成功重新计算。")
except Exception as e:
logger.error(f"重新计算 MACD 列时出错:{str(e)}")
raise ValueError("DataFrame 缺少必要的 MACD 列且无法重新计算。")
enter_long_conditions = [
(df["rsi"] < df["buy_rsi_pred"]), # RSI 低于买入阈值
(df["volume"] > df["volume"].rolling(window=10).mean() * 1.2), # 成交量高于近期均值20%
(df["close"] > df["bb_middleband"]), # 价格高于布林带中轨
(df["sentiment_score"] > self.sentiment_weight.value) # 市场情绪积极
]
# 如果 MACD 列存在,则添加 MACD 金叉条件
if "macd" in df.columns and "macdsignal" in df.columns:
enter_long_conditions.append((df["macd"] > df["macdsignal"]))
# 确保模型预测为买入
enter_long_conditions.append((df["do_predict"] == 1))
if enter_long_conditions:
df.loc[
reduce(lambda x, y: x & y, enter_long_conditions),
["enter_long", "enter_tag"]
] = (1, "long")
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
# 改进卖出信号条件
exit_long_conditions = [
(df["rsi"] > df["sell_rsi_pred"]), # RSI 高于卖出阈值
(df["volume"] > df["volume"].rolling(window=10).mean()), # 成交量高于近期均值
(df["close"] < df["bb_middleband"]), # 价格低于布林带中轨
(df["sentiment_score"] < self.sentiment_weight.value) # 市场情绪消极
]
if exit_long_conditions:
df.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
"exit_long"
] = 1
return df

View File

@ -1,189 +0,0 @@
from freqtrade.strategy import IStrategy
import talib.abstract as ta
import pandas as pd
import numpy as np
import logging
from technical import qtpylib
from functools import reduce
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
logger = logging.getLogger(__name__)
class MyDynamicStrategy(IStrategy):
# --- 参数空间 ---
buy_rsi = IntParameter(10, 50, default=30)
sell_rsi = IntParameter(50, 90, default=70)
roi_0 = DecimalParameter(0.01, 0.2, default=0.03)
roi_1 = DecimalParameter(0.005, 0.1, default=0.015)
stoploss_param = DecimalParameter(-0.3, -0.1, default=-0.15)
trailing_stop = True
trailing_stop_positive = 0.05
trailing_stop_positive_offset = 0.1
can_short = False
process_only_new_candles = True
use_exit_signal = True
stoploss = -0.15
minimal_roi = {"0": 0.03, "30": 0.01, "60": 0}
# --- Plotting config ---
plot_config = {
"main_plot": {},
"subplots": {
"RSI Buy Threshold": {
"&-buy_rsi": {"color": "green"}
},
"ROI and Stoploss": {
"&-roi_0": {"color": "orange"},
"&-stoploss": {"color": "red"}
}
}
}
# --- FreqAI 配置 ---
freqai_info = {
"model": "CatboostClassifier",
"feature_parameters": {
"include_timeframes": ["5m", "1h"],
"indicator_periods_candles": [10, 20, 50],
"include_corr_pairlist": ["BTC/USDT"],
"target_classifier": "value",
"label_period_candles": 20,
},
"training_settings": {
"train_period_days": 30,
"startup_candle_count": 200
}
}
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
df = dataframe.copy()
df[f'rsi_{period}'] = ta.RSI(df, timeperiod=period)
df[f'sma_diff_{period}'] = df['close'] - ta.SMA(df, timeperiod=period)
df[f'macd_{period}'], _, _ = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
df[f'stoch_rsi_{period}'] = ta.STOCHRSI(df, timeperiod=period)
df[f'cci_{period}'] = ta.CCI(df, timeperiod=period)
df[f'willr_{period}'] = ta.WILLR(df, timeperiod=period)
df[f'atr_{period}'] = ta.ATR(df, timeperiod=period)
df[f'price_change_rate_{period}'] = df['close'].pct_change(period)
df[f'volatility_{period}'] = df['close'].pct_change().rolling(window=period).std()
return df
def set_freqai_targets(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
# 使用短期和长期均线交叉作为目标标签
short_ma = ta.SMA(dataframe, timeperiod=10)
long_ma = ta.SMA(dataframe, timeperiod=50)
dataframe['target'] = np.where(short_ma > long_ma, 2,
np.where(short_ma < long_ma, 0, 1))
return dataframe
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
# 示例使用简单未来N周期涨跌作为目标变量
# 使用短期均线趋势代替未来价格
# 计算短期和长期均线
short_ma = ta.SMA(dataframe, timeperiod=10)
long_ma = ta.SMA(dataframe, timeperiod=50)
dataframe['short_ma'] = short_ma
dataframe['long_ma'] = long_ma
# 计算 RSI 和其他动态参数
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
dataframe['&-buy_rsi'] = self.buy_rsi.value
dataframe['&-sell_rsi'] = self.sell_rsi.value
dataframe['&-roi_0'] = self.roi_0.value
dataframe['&-stoploss'] = self.stoploss_param.value
# 添加调试日志
logging.info(f"Feature columns after feature engineering: {list(dataframe.columns)}")
# 使用短期和长期均线交叉作为目标标签
dataframe['target'] = np.where(short_ma > long_ma, 2,
np.where(short_ma < long_ma, 1, 0))
# 动态设置 minimal_roi
# 平滑处理 ROI 参数
# 基于波动率动态调整 ROI 参数
# 使用指数加权移动平均 (EWMA) 计算波动率
volatility = dataframe['close'].pct_change().ewm(span=20, adjust=False).std().mean()
roi_0_dynamic = max(0.01, min(0.2, self.roi_0.value * (1 + volatility)))
roi_1_dynamic = max(0.005, min(0.1, self.roi_1.value * (1 + volatility)))
self.minimal_roi = {
0: roi_0_dynamic,
30: roi_1_dynamic,
60: 0
}
# 动态调整止损距离
volatility_multiplier = max(1.5, min(3.0, 2.0 + volatility))
# 波动率倍数
self.stoploss = -0.15 * volatility_multiplier
# 计算 Bollinger Bands 并添加到 dataframe
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bollinger_upper'] = bollinger['upper']
dataframe['bollinger_mid'] = bollinger['mid']
dataframe['bollinger_lower'] = bollinger['lower']
# 计算 MACD 并添加到 dataframe
macd, macdsignal, _ = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd
dataframe['macdsignal'] = macdsignal
# 添加调试日志
logging.info(f"RSI condition: {(dataframe['rsi'] < dataframe['&-buy_rsi']).sum()}")
logging.info(f"Volume condition: {(dataframe['volume'] > dataframe['volume'].rolling(window=20).mean() * 1.05).sum()}")
logging.info(f"MACD condition: {((dataframe['close'] <= dataframe['bollinger_lower'] * 1.01) & (dataframe['macd'] > dataframe['macdsignal'])).sum()}")
# 添加 ADX 趋势过滤器
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
is_strong_trend = dataframe['adx'].iloc[-1] > 25
# MACD 穿越信号条件
(dataframe["close"] < dataframe['bollinger_lower']) & (dataframe['macd'] > dataframe['macdsignal']),
# 基于趋势强度动态调整追踪止损
trend_strength = (dataframe['short_ma'] - dataframe['long_ma']).mean()
if is_strong_trend:
self.trailing_stop_positive = max(0.01, min(0.1, abs(trend_strength) * 0.3))
self.trailing_stop_positive_offset = max(0.01, min(0.2, abs(trend_strength) * 0.6))
else:
self.trailing_stop_positive = 0.05
self.trailing_stop_positive_offset = 0.1
trend_strength = (dataframe['short_ma'] - dataframe['long_ma']).mean()
self.trailing_stop_positive = max(0.01, min(0.1, abs(trend_strength) * 0.3))
return dataframe
def populate_entry_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame:
# 增加成交量过滤和 Bollinger Bands 信号
# 计算 MACD
macd, macdsignal, _ = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
df['macd'] = macd
df['macdsignal'] = macdsignal
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(df), window=20, stds=2)
conditions = [
(df["rsi"] < df["&-buy_rsi"]), # RSI 低于买入阈值
(df["volume"] > df["volume"].rolling(window=20).mean() * 1.1), # 成交量增长超过 10%
(df["close"] < df['bollinger_lower']) & (df['macd'] > df['macdsignal']), # MACD 穿越信号
]
df.loc[reduce(lambda x, y: x & y, conditions), 'enter_long'] = 1
return df
def populate_exit_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame:
# 增加 Bollinger Bands 中轨信号
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(df), window=20, stds=2)
exit_long_conditions = [
(df["rsi"] > df["&-sell_rsi"]),
(df["close"] > df['bollinger_mid']) # Bollinger Bands 中轨信号
]
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), 'exit_long'] = 1
return df
def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force, current_time, entry_tag, side, **kwargs):
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = df.iloc[-1]
if rate > last_candle["close"] * 1.0025:
return False
return True

View File

@ -1,64 +0,0 @@
from freqtrade.strategy import IStrategy, CategoricalParameter, DecimalParameter, IntParameter
import pandas as pd
import numpy as np
import talib.abstract as ta
class MyHyperoptStrategy(IStrategy):
INTERFACE_VERSION = 3
# Buy hyperspace params:
buy_params = {
"ema_short_period": 10,
"ema_long_period": 50,
}
# Sell hyperspace params:
sell_params = {
"rsi_high": 70,
}
ema_short_period = IntParameter(5, 20, default=10, space="buy", optimize=True)
ema_long_period = IntParameter(40, 100, default=50, space="buy", optimize=True)
rsi_high = IntParameter(60, 85, default=70, space="sell", optimize=True)
# Minimal ROI designed for the strategy
minimal_roi = {
"0": 0.1
}
# Optimal stoploss designed for the strategy
stoploss = -0.10
# Trailing stop
trailing_stop = False
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
# Ensure only the 'close' column is passed to RSI
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
dataframe['ema_short'] = dataframe['close'].ewm(span=self.ema_short_period.value, adjust=False).mean()
dataframe['ema_long'] = dataframe['close'].ewm(span=self.ema_long_period.value, adjust=False).mean()
return dataframe
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe.loc[
(
(dataframe['ema_short'] > dataframe['ema_long']) &
(dataframe['rsi'] < 30)
),
'enter_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe.loc[
(
(dataframe['ema_short'] < dataframe['ema_long']) &
(dataframe['rsi'] > self.rsi_high.value)
),
'exit_long'
] = 1
return dataframe

View File

@ -1,709 +0,0 @@
import logging
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import pandas as pd
import numpy as np
import talib as ta
import datetime
from typing import Dict, List, Optional
from sklearn.metrics import mean_squared_error
from freqtrade.strategy import CategoricalParameter, DecimalParameter
from xgboost import XGBRegressor
import ccxt
logger = logging.getLogger(__name__)
class OKXRegressionStrategy(IStrategy):
"""
Freqtrade AI 策略使用回归模型进行 OKX 数据上的仅做多交易
- 数据通过 CCXT OKX 交易所获取
- 使用 XGBoost 回归模型预测价格变化
- 仅生成做多买入信号不做空
- 适配 Freqtrade 2025.3继承 IStrategy
"""
# 指标所需的最大启动蜡烛数
startup_candle_count: int = 20
# 策略元数据(建议通过 config.json 配置)
trailing_stop = True
trailing_stop_positive = 0.01
max_open_trades = 3
stake_amount = 'dynamic'
atr_period = CategoricalParameter([7, 14, 21], default=14, space='buy')
atr_multiplier = DecimalParameter(1.0, 3.0, default=2.0, space='sell')
# FreqAI 配置
freqai_config = {
"enabled": True,
"identifier": "okx_regression_v1",
"model_training_parameters": {
"n_estimators": 100,
"learning_rate": 0.05,
"max_depth": 6
},
"feature_parameters": {
"include_timeframes": ["5m", "15m", "1h"],
"include_corr_pairlist": ["BTC/USDT", "ETH/USDT"],
"label_period_candles": 12,
"include_shifted_candles": 2, # 添加历史偏移特征
"principal_component_analysis": True # 启用 PCA
},
"data_split_parameters": {
"test_size": 0.2,
"random_state": 42,
"shuffle": False
},
"train_period_days": 90,
"backtest_period_days": 30,
"purge_old_models": True # 清理旧模型
}
def __init__(self, config: Dict):
super().__init__(config)
# 初始化特征缓存
self.feature_cache = {}
# 设置日志级别
logger.setLevel(logging.DEBUG)
# 输出模型路径用于调试
freqai_model_path = self.config.get("freqai", {}).get("model_path", "/freqtrade/user_data/models")
logger.info(f"FreqAI 模型路径:{freqai_model_path}")
# 新增模型加载状态日志
model_identifier = self.freqai_config.get("identifier", "default")
model_file = f"{freqai_model_path}/{model_identifier}/cb_okb_*.pkl"
import os
if any(os.path.exists(f.replace('*', '1743465600')) for f in [model_file]):
logger.info("✅ 模型文件存在,准备加载")
else:
logger.warning("⚠️ 模型文件未找到,请确认是否训练完成")
logger.info(f"🔍 正在尝试从 {freqai_model_path} 加载模型")
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: Dict, **kwargs) -> DataFrame:
"""
为每个时间框架和相关交易对生成特征
"""
cache_key = f"{metadata.get('pair', 'unknown')}_{period}"
if cache_key in self.feature_cache:
logger.debug(f"使用缓存特征:{cache_key}")
return self.feature_cache[cache_key].copy()
# RSI
dataframe[f"rsi_{period}"] = ta.RSI(dataframe["close"], timeperiod=period)
# MACD
macd, macdsignal, _ = ta.MACD(dataframe["close"], fastperiod=12, slowperiod=26, signalperiod=9)
dataframe[f"%-%-macd-{period}"] = macd
dataframe[f"%-%-macdsignal-{period}"] = macdsignal
# 布林带宽度
upper, middle, lower = ta.BBANDS(dataframe["close"], timeperiod=period)
dataframe[f"%-%-bb_width-{period}"] = (upper - lower) / middle
# 成交量均线
dataframe[f"%-%-volume_ma-{period}"] = ta.SMA(dataframe["volume"], timeperiod=period)
# 仅为 BTC/USDT 和 ETH/USDT 生成 order_book_imbalance
#
pair = metadata.get('pair', 'unknown')
# 注释掉订单簿相关代码
# if pair in ["BTC/USDT", "ETH/USDT"]:
# try:
# order_book = self.fetch_okx_order_book(pair)
# dataframe[f"%-%-order_book_imbalance"] = (
# order_book["bids"] - order_book["asks"]
# ) / (order_book["bids"] + order_book["asks"] + 1e-10)
# except Exception as e:
# logger.warning(f"Failed to fetch order book for {pair}: {str(e)}")
# dataframe[f"%-%-order_book_imbalance"] = 0.0
# 数据清洗
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
dataframe = dataframe.ffill().fillna(0)
# 缓存特征副本,避免引用污染
self.feature_cache[cache_key] = dataframe.copy()
logger.debug(f"周期 {period} 特征:{list(dataframe.filter(like='%-%-').columns)}")
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
"""
添加基础时间框架的全局特征
"""
# 确保索引是 DatetimeIndex
if not isinstance(dataframe.index, pd.DatetimeIndex):
dataframe = dataframe.set_index(pd.DatetimeIndex(dataframe.index))
# 价格变化率
dataframe["%-price_change"] = dataframe["close"].pct_change()
# 时间特征:小时
dataframe["%-hour_of_day"] = dataframe.index.hour / 24.0
# 数据清洗
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
dataframe = dataframe.ffill().fillna(0)
logger.debug(f"全局特征:{list(dataframe.filter(like='%-%-').columns)}")
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
设置回归模型的目标变量为不同币对设置动态止损和ROI阈值
输入dataframeK线数据close, high, lowmetadata交易对信息configFreqAI配置
输出更新后的dataframe包含目标标签
"""
# 等待模型加载完成
import time
while not hasattr(self, "freqai") or self.freqai is None:
time.sleep(1)
# 获取配置参数
label_period = self.freqai_config["feature_parameters"]["label_period_candles"] # 标签预测周期如5分钟K线的N根
pair = metadata["pair"] # 当前交易对如DOGE/USDT
# 计算未来价格变化率(现有逻辑)
dataframe["&-s_close"] = (dataframe["close"].shift(-label_period) - dataframe["close"]) / dataframe["close"]
# 计算不同时间窗口的ROI现有逻辑
for minutes in [0, 15, 30]:
candles = int(minutes / 5) # 假设5分钟K线
if candles > 0:
dataframe[f"&-roi_{minutes}"] = (dataframe["close"].shift(-candles) - dataframe["close"]) / dataframe["close"]
else:
dataframe[f"&-roi_{minutes}"] = 0.0
# 计算市场状态指标ADX14周期与label_period_candles对齐
dataframe["adx"] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
# 定义币对特定的ADX阈值和止损/ROI范围
pair_thresholds = {
"DOGE/USDT": {
"adx_trend": 20,
"adx_oscillation": 15,
"stoploss_trend": -0.08,
"stoploss_oscillation": -0.04,
"stoploss_mid": -0.06,
"roi_trend": 0.06,
"roi_oscillation": 0.025,
"roi_mid": 0.04
},
"BTC/USDT": {
"adx_trend": 25,
"adx_oscillation": 20,
"stoploss_trend": -0.03,
"stoploss_oscillation": -0.015,
"stoploss_mid": -0.02,
"roi_trend": 0.03,
"roi_oscillation": 0.015,
"roi_mid": 0.02
},
"SOL/USDT": {
"adx_trend": 22,
"adx_oscillation": 18,
"stoploss_trend": -0.06,
"stoploss_oscillation": -0.03,
"stoploss_mid": -0.045,
"roi_trend": 0.045,
"roi_oscillation": 0.02,
"roi_mid": 0.03
},
"XRP/USDT": {
"adx_trend": 22,
"adx_oscillation": 18,
"stoploss_trend": -0.06,
"stoploss_oscillation": -0.03,
"stoploss_mid": -0.045,
"roi_trend": 0.045,
"roi_oscillation": 0.02,
"roi_mid": 0.03
},
"OKB/USDT": {
"adx_trend": 18,
"adx_oscillation": 12,
"stoploss_trend": -0.10,
"stoploss_oscillation": -0.06,
"stoploss_mid": -0.08,
"roi_trend": 0.07,
"roi_oscillation": 0.04,
"roi_mid": 0.05
}
}
# 设置回归目标
label_period = self.freqai_config["feature_parameters"]["label_period_candles"]
dataframe["&-s_close"] = (dataframe["close"].shift(-label_period) - dataframe["close"]) / dataframe["close"]
# 设置动态 ROI 和止损阈值
dataframe["&-roi_0_pred"] = 0.03 # 默认值,防止空值
dataframe["&-stoploss_pred"] = -0.05
for index, row in dataframe.iterrows():
thresholds = pair_thresholds.get(pair, {})
if not thresholds:
continue
adx_value = row["adx"]
if adx_value > thresholds["adx_trend"]: # 趋势市场
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"] # 宽松止损
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"] # 严格止损
else:
# 中性市场:使用中间值
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"]
for index, row in dataframe.iterrows():
thresholds = pair_thresholds.get(pair, {})
if not thresholds:
continue
adx_value = row["adx"]
if adx_value > thresholds["adx_trend"]: # 趋势市场
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"] # 较高ROI目标
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"] # 较低ROI目标
else:
# 中性市场:使用中间值
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"]
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
"""
使用 FreqAI 生成指标和预测
"""
try:
logger.debug(f"FreqAI 对象:{type(self.freqai)}")
dataframe = self.freqai.start(dataframe, metadata, self)
# 强制刷新缓存,避免旧数据干扰
if hasattr(self, 'feature_cache'):
self.feature_cache.clear()
# 验证数据完整性
if dataframe["close"].isna().any() or dataframe["volume"].isna().any():
logger.warning("检测到 OKX 数据缺失,使用前向填充")
dataframe = dataframe.ffill().fillna(0)
# 检查所有目标列是否都存在
required_targets = ["&-s_close", "&-roi_0", "&-buy_rsi_pred", "&-stoploss_pred", "&-roi_0_pred"]
missing_targets = [col for col in required_targets if col not in dataframe.columns]
if missing_targets:
logger.warning(f"⚠️ 缺失目标列:{missing_targets},尝试重新生成")
# 检查预测列
required_preds = ['pred_upper', 'pred_lower']
missing_preds = [col for col in required_preds if col not in dataframe.columns]
if missing_preds:
logger.warning(f"⚠️ 缺失预测列:{missing_preds},尝试回退默认值")
for col in missing_preds:
dataframe[col] = np.nan
# 预测统计
if "&-s_close" in dataframe.columns:
logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, 方差={dataframe['&-s_close'].var():.4f}")
if dataframe["pred_upper"].isna().all():
logger.warning("⚠️ pred_upper 列全为 NaN可能模型未加载成功")
logger.debug(f"生成的列:{list(dataframe.columns)}")
except Exception as e:
logger.error(f"FreqAI start 失败:{str(e)}")
raise
finally:
logger.debug("populate_indicators 完成")
# 确保返回 DataFrame防止 None
if dataframe is None:
dataframe = DataFrame()
# 添加 ATR 指标
dataframe['ATR_{}'.format(self.atr_period.value)] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
# 添加 RSI 和布林带指标
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
upper, middle, lower = ta.BBANDS(dataframe['close'], timeperiod=20, nbdevup=2, nbdevdn=2)
dataframe['bb_upper'] = upper
dataframe['bb_middle'] = middle
dataframe['bb_lower'] = lower
# 添加高时间框架趋势1小时均线
dataframe_1h = None
if self.config['timeframe'] != '1h':
dataframe_1h = self.dp.get_analyzed_dataframe(metadata['pair'], '1h')[0]
if dataframe_1h is not None and not dataframe_1h.empty and 'close' in dataframe_1h.columns:
dataframe['trend_1h'] = dataframe_1h['close'].rolling(window=20).mean()
else:
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
else:
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
# 强制检查预测列是否存在,若不存在则填充 NaN 并记录警告
required_pred_cols = ['pred_upper', 'pred_lower', '&-s_close', '&-roi_0_pred']
missing_pred_cols = [col for col in required_pred_cols if col not in dataframe.columns]
if missing_pred_cols:
logger.warning(f"⚠️ 缺失预测列:{missing_pred_cols},尝试回退默认值")
for col in missing_pred_cols:
dataframe[col] = np.nan
else:
logger.debug("✅ 预测列已就绪:")
logger.debug(dataframe[['&-s_close', 'pred_upper', 'pred_lower']].head().to_string())
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
"""
基于模型预测和 RSI 生成做多信号
"""
# 确保必要列存在
required_cols = ['rsi', 'volume', '&-s_close', '&-roi_0_pred']
for col in required_cols:
if col not in dataframe.columns:
dataframe[col] = np.nan
# 只在预测值有效时生成信号
valid_prediction = (~dataframe['&-s_close'].isna()) & (dataframe['&-s_close'] > 0.005) & (dataframe['&-s_close'].abs() > 0.001)
# 结合 RSI 和模型预测生成信号
dataframe.loc[
(dataframe["rsi"] < 30) &
(dataframe["volume"] > 0) &
valid_prediction,
"enter_long"
] = 1
# 设置 entry_price 列,用于止损逻辑
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
logger.debug(f"生成 {dataframe['enter_long'].sum()} 个做多信号")
return dataframe
def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
"""
封装动态止损逻辑基于入场价和ATR计算止损线
:param dataframe: 原始DataFrame
:param metadata: 策略元数据
:param atr_col: 使用的ATR列名
:param multiplier: ATR乘数
:return: 更新后的DataFrame
"""
# 获取交易对信息
pair = metadata.get('pair', 'unknown')
# 设置默认的止损倍数
stop_loss_multiplier = 2.0
# 根据交易对调整止损倍数示例BTC/USDT 更稳定,止损倍数较低)
if pair == "BTC/USDT":
stop_loss_multiplier = 1.5
elif pair == "DOGE/USDT":
stop_loss_multiplier = 2.5
elif pair == "SOL/USDT":
stop_loss_multiplier = 2.0
elif pair == "XRP/USDT":
stop_loss_multiplier = 2.0
# 计算止损线
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
dataframe['stop_loss_line'] = dataframe['entry_price'] - dataframe[atr_col] * stop_loss_multiplier
# 应用止损逻辑OKB/USDT 使用更平滑的退出)
if metadata.get('pair') == 'OKB/USDT':
# 对 OKB 添加缓冲区,避免频繁触发
buffer_ratio = 0.005 # 0.5% 缓冲
buffered_stop_loss = dataframe['stop_loss_line'] * (1 - buffer_ratio)
dataframe.loc[
(dataframe['close'] < buffered_stop_loss),
'exit_long'
] = 1
else:
dataframe.loc[
(dataframe['close'] < dataframe['stop_loss_line']),
'exit_long'
] = 1
return dataframe
def _dynamic_take_profit(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
"""
封装动态止盈逻辑基于入场价ATR ADX 调整止盈线
:param dataframe: 原始DataFrame
:param metadata: 策略元数据
:param atr_col: 使用的ATR列名
:param multiplier: ATR乘数基础值
:return: 更新后的DataFrame
"""
# 获取交易对信息
pair = metadata.get('pair', 'unknown')
# 设置默认的止盈倍数
take_profit_multiplier = 2.0
# 根据交易对调整止盈倍数示例BTC/USDT 更稳定,止盈倍数较高)
if pair == "BTC/USDT":
take_profit_multiplier = 3.0
elif pair == "DOGE/USDT":
take_profit_multiplier = 1.5
elif pair == "SOL/USDT":
take_profit_multiplier = 2.0
elif pair == "XRP/USDT":
take_profit_multiplier = 2.0
# 计算当前ATR在历史窗口中的百分位
historical_atr = dataframe[atr_col].rolling(window=20).mean().dropna().values
if len(historical_atr) < 20:
return dataframe
current_atr = dataframe[atr_col].iloc[-1]
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
# 根据波动率百分位调整止盈倍数
if percentile > 80: # 高波动市场,缩短止盈距离
volatility_adjustment = 0.8
elif percentile < 20: # 低波动市场,拉长止盈距离
volatility_adjustment = 1.2
else: # 正常波动市场,保持默认
volatility_adjustment = 1.0
# 获取ADX指标判断趋势强度
dataframe['adx'] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
adx_value = dataframe['adx'].iloc[-1]
# 根据ADX趋势强度调整止盈倍数
if adx_value > 25: # 强趋势,延长止盈距离
trend_adjustment = 1.3
elif adx_value < 15: # 震荡行情,缩短止盈距离
trend_adjustment = 0.7
else: # 中性趋势,保持默认
trend_adjustment = 1.0
# 综合调整止盈倍数
adjusted_multiplier = take_profit_multiplier * volatility_adjustment * trend_adjustment
# 计算止盈线
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe[atr_col] * adjusted_multiplier
# 应用止盈逻辑
dataframe.loc[
(dataframe['close'] > dataframe['take_profit_line']),
'exit_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
# 确保 ATR 列存在
if 'ATR_14' not in dataframe.columns:
dataframe['ATR_14'] = 0.0
# 应用动态止损和止盈逻辑
dataframe = self._dynamic_stop_loss(dataframe, metadata)
dataframe = self._dynamic_take_profit(dataframe, metadata)
return dataframe
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], **kwargs) -> float:
"""
动态下注每笔交易占账户余额 2%
"""
balance = self.wallets.get_available_stake_amount()
stake = balance * 0.02
return min(max(stake, min_stake), max_stake)
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, profit_percent: float,
after_fill: bool, **kwargs) -> Optional[float]:
"""
自适应止损基于市场波动率百分位动态调整ATR乘数
"""
if trade.enter_tag == 'long':
# 获取多个周期的ATR值
dataframe = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)
# 计算不同周期的ATR
dataframe['ATR_7'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=7)
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
dataframe['ATR_21'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=21)
# 计算20日平均ATR用于百分位计算
avg_atr_window = 20
dataframe['avg_atr'] = dataframe['ATR_14'].rolling(window=avg_atr_window).mean()
# 获取最新数据
latest_row = dataframe.iloc[-1].copy()
# 计算当前ATR在历史窗口中的百分位
historical_atr = dataframe['avg_atr'].dropna().values
if len(historical_atr) < avg_atr_window:
return None
current_atr = latest_row['avg_atr']
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
# 根据市场波动率百分位选择ATR乘数
if percentile > 80: # 高波动市场
atr_multiplier = 1.5
elif percentile < 20: # 低波动市场
atr_multiplier = 2.5
else: # 正常波动市场
atr_multiplier = 2.0
# 根据交易对调整基础ATR值
pair_specific_atr = {
"BTC/USDT": latest_row['ATR_14'],
"ETH/USDT": latest_row['ATR_14'],
"OKB/USDT": latest_row['ATR_14'], # 使用更长周期 ATR 减少波动影响
"TON/USDT": latest_row['ATR_7']
}
if pair in pair_specific_atr:
base_atr = pair_specific_atr[pair]
else:
base_atr = latest_row['ATR_14']
# 计算追踪止损价格
trailing_stop = current_rate - base_atr * atr_multiplier
# 添加额外条件:确保止损不低于入场价的一定比例
min_profit_ratio = 0.005 # 最低盈利0.5%
min_stop_price = trade.open_rate * (1 + min_profit_ratio)
final_stop_price = max(trailing_stop, min_stop_price)
return final_stop_price / current_rate - 1 # 返回相对百分比
return None
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
禁用杠杆仅做多
"""
return 1.0
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
"""
验证交易进入检查 OKX 数据新鲜度
"""
if not self.check_data_freshness(pair, current_time):
logger.warning(f"{pair} 的 OKX 数据过期,跳过交易")
return False
return True
def check_data_freshness(self, pair: str, current_time: 'datetime') -> bool:
"""
简化版数据新鲜度检查不依赖外部 API
"""
# 假设数据总是新鲜的(用于测试)
return True
def fit(self, data_dictionary: Dict, metadata: Dict, **kwargs) -> None:
"""
训练回归模型并记录性能
"""
try:
# 初始化模型
if not hasattr(self, 'model') or self.model is None:
model_params = self.freqai_config["model_training_parameters"]
self.model = XGBRegressor(**model_params)
logger.debug("初始化新的 XGBoost 回归模型")
# 调用 FreqAI 训练
self.freqai.fit(data_dictionary, metadata, **kwargs)
# 记录训练集性能
train_data = data_dictionary["train_features"]
train_labels = data_dictionary["train_labels"]
train_predictions = self.model.predict(train_data)
train_mse = mean_squared_error(train_labels, train_predictions)
logger.info(f"训练集 MSE{train_mse:.6f}")
# 记录测试集性能(如果可用)
if "test_features" in data_dictionary:
test_data = data_dictionary["test_features"]
test_labels = data_dictionary["test_labels"]
test_predictions = self.model.predict(test_data)
test_mse = mean_squared_error(test_labels, test_predictions)
logger.info(f"测试集 MSE{test_mse:.6f}")
# 特征重要性
if hasattr(self.model, 'feature_importances_'):
importance = self.model.feature_importances_
logger.debug(f"特征重要性:{dict(zip(train_data.columns, importance))}")
except Exception as e:
logger.error(f"FreqAI fit 失败:{str(e)}")
raise
raise RuntimeError("模型训练失败,请检查数据完整性或重新训练模型")
def _callback_stop_loss(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
动态回调止损逻辑基于ATR调整回撤阈值并结合RSI和布林带过滤信号
"""
pair = metadata.get('pair', 'unknown')
# 设置默认参数
atr_col = 'ATR_14'
rolling_high_period = 20
rsi_overbought = 70
# 设置不同币种的回调乘数
callback_multipliers = {
"BTC/USDT": 1.5,
"ETH/USDT": 2.0,
"OKB/USDT": 1.3,
"TON/USDT": 2.0,
}
callback_multiplier = callback_multipliers.get(pair, 2.0)
# 确保ATR列存在
if atr_col not in dataframe.columns:
dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
# 计算动态回调百分比基于ATR
dataframe['callback_threshold'] = dataframe[atr_col] * callback_multiplier
# 计算ATR
if 'ATR_14' not in dataframe.columns:
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
# 计算最近高点
dataframe['rolling_high'] = dataframe['close'].rolling(window=20).max()
# 计算回调阈值
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe['ATR_14'] * callback_multiplier
# 应用止盈逻辑
dataframe.loc[
(dataframe['close'] > dataframe['take_profit_line']),
'exit_long'
] = 1
# 计算当前价格相对于最近高点的回撤比例使用ATR标准化
dataframe['callback_ratio'] = (dataframe['close'] - dataframe['rolling_high']) / dataframe['rolling_high']
dataframe['callback_condition_atr'] = (dataframe['close'] - dataframe['rolling_high']) <= -dataframe['callback_threshold']
# 获取RSI和布林带信息
dataframe['in_overbought'] = dataframe['rsi'] > rsi_overbought
dataframe['below_bb_upper'] = dataframe['close'] < dataframe['bb_upper']
# 获取高时间框架趋势1小时均线
dataframe['trend_up'] = dataframe['close'] > dataframe['trend_1h']
dataframe['trend_down'] = dataframe['close'] < dataframe['trend_1h']
# 综合回调止损条件
callback_condition = (
dataframe['callback_condition_atr'] &
((dataframe['in_overbought'] | (~dataframe['below_bb_upper']))) &
dataframe['trend_down']
)
# 应用回调止损逻辑
dataframe.loc[callback_condition, 'exit_long'] = 1
return dataframe

View File

@ -1,192 +0,0 @@
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
from pandas import DataFrame
import pandas as pd
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy as np
from typing import Dict, List, Optional
import logging
logger = logging.getLogger(__name__)
class TheForceFreqaiStrategy(IStrategy):
timeframe = '5m'
minimal_roi = {"0": 0.05, "60": 0.03, "120": 0.01}
stoploss = -0.15
process_only_new_candles = True
use_exit_signal = True
startup_candle_count = 200
max_open_trades = 4
buy_macd_diff = DecimalParameter(-10.0, 10.0, default=0.0, space="buy")
sell_stoch_overbought = IntParameter(70, 90, default=80, space="sell")
freqai = {
"enabled": True,
"model": "LightGBMRegressor",
"train_period_days": 7,
"backtest_period_days": 2,
"identifier": "theforce_model",
"feature_params": {
"include_timeframes": ["5m"],
"include_corr_pairlist": [],
"label_period_candles": 5,
"include_shifted_candles": 2,
"DI_threshold": 1.5,
"include_default_features": ["open", "high", "low", "close", "volume"],
"use_SVM_to_remove_outliers": False
},
"data_split_parameters": {
"test_size": 0.2,
"random_state": 42
},
"model_training_parameters": {
"n_estimators": 100,
"max_depth": 7,
"verbose": -1
},
"live_retrain": True,
"purge_old_models": True,
"fit_live_predictions_candles": 50,
"data_kitchen_thread_count": 2,
"force_train": True,
"verbose": 2,
"save_backtest_models": True
}
def __init__(self, config: Dict):
super().__init__(config)
self.feature_cache = {}
self.freqai_config = self.freqai
logging.getLogger('').setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
for log in ['freqtrade.freqai', 'freqtrade.freqai.data_kitchen',
'freqtrade.freqai.data_drawer', 'freqtrade.freqai.prediction_models']:
logging.getLogger(log).setLevel(logging.DEBUG)
logger.info("Initialized TheForceFreqaiStrategy with DEBUG logging")
def _normalize_column(self, series: pd.Series) -> pd.Series:
if series.nunique() <= 1:
return pd.Series(np.zeros_like(series), index=series.index)
min_val = series.min()
max_val = series.max()
normalized = (series - min_val) / (max_val - min_val)
return normalized.fillna(0)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.debug(f"Populating indicators for pair {metadata['pair']}")
logger.debug(f"Input dataframe shape: {dataframe.shape}, columns: {list(dataframe.columns)}")
logger.debug(f"First 5 rows:\n{dataframe.head().to_string()}")
dataframe = self.freqai.start(dataframe, metadata, self)
logger.info("FreqAI start called, checking dataframe columns: %s", list(dataframe.columns))
required_columns = ['open', 'high', 'low', 'close', 'volume']
missing_columns = [col for col in required_columns if col not in dataframe.columns]
if missing_columns:
logger.error(f"Missing columns in dataframe: {missing_columns}")
return dataframe
if dataframe.empty or len(dataframe) < self.startup_candle_count:
logger.error(f"Insufficient data: {len(dataframe)} candles, required {self.startup_candle_count}")
return dataframe
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'].fillna(0)
dataframe['macdsignal'] = macd['macdsignal'].fillna(0)
dataframe['macd_diff'] = macd['macd'] - macd['macdsignal']
stoch = ta.STOCH(dataframe)
dataframe['slowk'] = stoch['slowk'].fillna(0)
dataframe['slowd'] = stoch['slowd'].fillna(0)
dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=12).fillna(dataframe['close'])
dataframe['ema_long'] = ta.EMA(dataframe, timeperiod=26).fillna(dataframe['close'])
logger.debug(f"Indicators generated: {list(dataframe.columns)}")
logger.debug(f"Indicator sample:\n{dataframe[['macd', 'slowk', 'ema_short']].tail().to_string()}")
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.debug(f"Setting FreqAI targets for pair {metadata['pair']}")
label_candles = self.freqai_config['feature_params']['label_period_candles']
dataframe['&-target'] = (
dataframe['close'].shift(-label_candles) / dataframe['close'] - 1
).fillna(0)
valid_targets = len(dataframe['&-target'].dropna())
logger.debug(f"Target '&-target' generated with {valid_targets} valid values")
logger.debug(f"Target stats: min={dataframe['&-target'].min()}, max={dataframe['&-target'].max()}, mean={dataframe['&-target'].mean()}")
logger.debug(f"Target sample:\n{dataframe['&-target'].tail().to_string()}")
return dataframe
def feature_engineering_expand(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.debug(f"Expanding features for pair {metadata['pair']}")
dataframe['%-rsi'] = ta.RSI(dataframe, timeperiod=14)
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['%-pct_change'] = dataframe['close'].pct_change()
dataframe['%-volume_change'] = dataframe['volume'].pct_change(periods=5)
for col in dataframe.columns:
if col.startswith('%-'):
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill().fillna(0)
if dataframe[col].nunique() > 1:
dataframe[f"{col}_norm"] = self._normalize_column(dataframe[col])
else:
dataframe[f"{col}_norm"] = 0
logger.debug(f"Features generated: {list(dataframe.columns)}")
logger.debug(f"Feature sample:\n{dataframe[[c for c in dataframe.columns if c.startswith('%-')]].tail().to_string()}")
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.debug(f"Standard feature engineering for pair {metadata['pair']}")
dataframe['%-rsi'] = ta.RSI(dataframe, timeperiod=14)
dataframe['%-day_of_week'] = dataframe['date'].dt.dayofweek
dataframe['%-hour_of_day'] = dataframe['date'].dt.hour
for col in dataframe.columns:
if col.startswith('%-'):
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
dataframe[col] = dataframe[col].ffill().fillna(0)
logger.debug(f"Standard features generated: {list(dataframe.columns)}")
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.debug(f"Populating entry trend for pair {metadata['pair']}")
logger.debug(f"Columns available: {list(dataframe.columns)}")
logger.debug(f"&-target stats: {dataframe['&-target'].describe()}")
logger.debug(f"slowk stats: {dataframe['slowk'].describe()}")
logger.debug(f"macd_diff stats: {dataframe['macd_diff'].describe()}")
if '&-target' not in dataframe.columns:
logger.warning(f"'&-target' column missing for pair {metadata['pair']}, using indicators only")
dataframe.loc[
(dataframe['macd'] > dataframe['macdsignal']) &
(dataframe['macd_diff'] > self.buy_macd_diff.value) &
(dataframe['slowk'] < 30) &
(dataframe['ema_short'] > dataframe['ema_long']),
'enter_long'] = 1
logger.debug(f"Fallback entry signals: {dataframe['enter_long'].sum()} buys")
return dataframe
dataframe.loc[
(dataframe['macd'] > dataframe['macdsignal']) &
(dataframe['macd_diff'] > self.buy_macd_diff.value) &
(dataframe['slowk'] < 30) &
(dataframe['ema_short'] > dataframe['ema_long']) &
(dataframe['&-target'] > 0.005),
'enter_long'] = 1
logger.debug(f"FreqAI entry signals: {dataframe['enter_long'].sum()} buys")
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.debug(f"Populating exit trend for pair {metadata['pair']}")
dataframe.loc[
(dataframe['macd'] < dataframe['macdsignal']) &
(dataframe['slowk'] > self.sell_stoch_overbought.value),
'exit_long'] = 1
return dataframe

View File

@ -0,0 +1,29 @@
{
"strategy_name": "TheForceV7",
"params": {
"roi": {},
"trailing": {
"trailing_stop": false,
"trailing_stop_positive": null,
"trailing_stop_positive_offset": 0.0,
"trailing_only_offset_is_reached": false
},
"max_open_trades": {
"max_open_trades": 3
},
"buy": {
"adx_buy": 25,
"atr_ratio": 0.005
},
"sell": {
"ema_fast_period": 7,
"rsi_sell": 60
},
"protection": {},
"stoploss": {
"stoploss": -0.061
}
},
"ft_stratparam_v": 1,
"export_time": "2025-05-15 12:57:22.019801+00:00"
}

View File

@ -1,18 +1,44 @@
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta
from typing import Optional, Union
from typing import Dict, List, Optional, Union
from freqtrade.persistence import Trade
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
from datetime import datetime
class TheForceV7(IStrategy):
# 基础参数
timeframe = '5m'
stoploss = -0.14 # 全局止损
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# # Hyperopt 参数
# stoploss = DecimalParameter(
# low=-0.3, high=-0.1, default=-0.233, decimals=3, space='`stoploss'
# )
# profit_threshold_multiplier = DecimalParameter(
# low=0.1, high=1.0, default=0.5, decimals=3, space='stoploss'
# )
# trailing_stop_multiplier = DecimalParameter(
# low=0.5, high=3.0, default=1.5, decimals=3, space='stoploss'
# )
# Hyperopt 参数
stoploss = DecimalParameter(low=-0.3, high=-0.1, default=-0.233, decimals=3, space='stoploss')
profit_threshold_multiplier = DecimalParameter(low=0.2, high=1.0, default=0.5, decimals=3, space='stoploss') # 扩大范围
trailing_stop_multiplier = DecimalParameter(low=0.8, high=3.0, default=1.5, decimals=3, space='stoploss') # 扩大范围
# Hyperopt 参数
stoploss = DecimalParameter(-0.3, -0.05, default=-0.15, decimals=3, space='stoploss')
profit_threshold_multiplier = DecimalParameter(0.8, 3.0, default=1.5, decimals=3, space='stoploss')
trailing_stop_multiplier = DecimalParameter(1.5, 6.0, default=3.0, decimals=3, space='stoploss')
adx_buy = DecimalParameter(15, 35, default=25, decimals=0, space='buy')
atr_ratio = DecimalParameter(0.002, 0.01, default=0.005, decimals=3, space='buy')
rsi_sell = DecimalParameter(50, 70, default=60, decimals=0, space='sell')
ema_fast_period = IntParameter(3, 10, default=7, space='sell')
@property
def protections(self):
return [
@ -39,18 +65,23 @@ class TheForceV7(IStrategy):
"lookback_period_candles": 6,
"trade_limit": 2,
"stop_duration_candles": 60,
"required_profit": 0.02 # 最低平均收益 2%
"required_profit": 0.02
},
{
"method": "LowProfitPairs",
"lookback_period_candles": 24,
"trade_limit": 4,
"stop_duration_candles": 2,
"required_profit": 0.01 # 最低平均收益 1%
"required_profit": 0.01
}
]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Calculate ATR (14-period, consistent with your code)
# Store the latest ATR in metadata for the pair
dataframe['ema200c'] = ta.EMA(dataframe['close'], timeperiod=200)
dataframe['ema50c'] = ta.EMA(dataframe['close'], timeperiod=50)
dataframe['ema20c'] = ta.EMA(dataframe['close'], timeperiod=20)
@ -61,9 +92,16 @@ class TheForceV7(IStrategy):
stoch = ta.STOCH(dataframe['high'], dataframe['low'], dataframe['close'], fastk_period=14, slowk_period=3, slowd_period=3)
dataframe['slowk'] = stoch[0]
dataframe['slowd'] = stoch[1]
dataframe['adx'] = ta.ADX(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
dataframe['volvar'] = dataframe['volume'].rolling(window=20).mean()
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
for period in [self.ema_fast_period.value, 21, 50]:
dataframe[f'ema_{period}'] = ta.EMA(dataframe, timeperiod=period)
metadata['latest_atr'] = dataframe['atr'].iloc[-1]
metadata['latest_adx'] = dataframe['adx'].iloc[-1]
return dataframe
def crossover(self, series1: DataFrame, series2: DataFrame) -> DataFrame:
@ -77,7 +115,7 @@ class TheForceV7(IStrategy):
(dataframe['rsi7'] < 50) & # Relaxed RSI
(dataframe['macd'] > 0) & # Relaxed MACD
(dataframe['volume'] > dataframe['volvar'] * 0.5) & # Relaxed volume
(dataframe['adx'] > 17), # Trend strength
(dataframe['adx'] > 21), # Trend strength
'enter_long'
] = 1
@ -97,43 +135,38 @@ class TheForceV7(IStrategy):
return dataframe
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty or dataframe['date'].iloc[-1] < current_time: # Fixed: Use date column
return None
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> Optional[float]:
atr = kwargs.get('metadata', {}).get('latest_atr', 0.01 * current_rate)
adx = kwargs.get('metadata', {}).get('latest_adx', 0)
atr_percent = atr / current_rate
profit_threshold = float(self.profit_threshold_multiplier.value) * atr_percent
trailing_stop_distance = float(self.trailing_stop_multiplier.value) * atr_percent
trade_duration = (current_time - trade.open_date).total_seconds() / 3600
last_candle = dataframe.iloc[-1]
atr = ta.ATR(dataframe, timeperiod=14).iloc[-1]
duration = (current_time - trade.open_date).total_seconds() / 60 # Minutes
# 动态调整
if trade_duration < 3: # 3小时内放宽
profit_threshold *= 1.5 # 提高触发门槛
trailing_stop_distance *= 1.5 # 放宽止损距离
if adx > 35: # 强趋势
trailing_stop_distance *= 0.5 # 紧跟趋势
elif adx < 20: # 震荡市场
trailing_stop_distance *= 2.0 # 放宽止损
# Dynamic Take-Profit
take_profit = current_rate + 1.0 * atr # Lowered ATR
if current_rate >= take_profit:
return "take_profit"
# ATR 追踪止损
if adx > 35 and after_fill and current_profit > profit_threshold:
return -trailing_stop_distance
# 固定止损
elif trade_duration > 1.5 or adx < 20 or current_profit < -0.015: # 1.5小时,震荡,亏损>1.5%
return float(self.stoploss)
return -0.05 # 默认止损收紧
# Partial Take-Profit at 2%
if current_profit >= 0.02:
return "partial_take_profit"
# Dynamic Stop-Loss
stop_loss = current_rate - 2.0 * atr # Relaxed ATR
if current_rate <= stop_loss:
return "dynamic_stop_loss"
# Trailing Stop
if current_profit > 0.005: # Lowered threshold
self.trailing_stop = True
self.trailing_stop_positive = 0.003 # 0.3% retracement
self.trailing_stop_positive_offset = 0.008 # 0.8% offset
if current_profit < trade.max_profit - self.trailing_stop_positive:
return "trailing_stop"
# Time Stop
if duration > 60: # 1 hour
return "time_stop"
return None
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, config: Dict, processed: Dict[str, DataFrame], *args, **kwargs) -> float:
total_profit = results['profit_abs'].sum()
win_rate = len(results[results['profit_abs'] > 0]) / trade_count if trade_count > 0 else 0
avg_duration = results['trade_duration'].mean() / 60 # 分钟
loss = -total_profit * win_rate * (1 / (avg_duration + 1))
return loss
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
@ -143,7 +176,7 @@ class TheForceV7(IStrategy):
if dataframe.empty:
return proposed_stake
atr = ta.ATR(dataframe, timeperiod=14).iloc[-1]
atr = ta.ATR(dataframe, timeperiod=28).iloc[-1]
price_std = dataframe['close'].std()
combined_volatility = atr + price_std
@ -157,3 +190,4 @@ class TheForceV7(IStrategy):
risk_factor = 1.2 if pair in ['BTC/USDT', 'ETH/USDT'] else 1.0
return base_stake * risk_factor

View File

@ -1,183 +0,0 @@
```python
class FreqaiExampleStrategy(IStrategy):
minimal_roi = {}
stoploss = -0.1
trailing_stop = True
process_only_new_candles = True
use_exit_signal = True
startup_candle_count: int = 100 # 增加数据需求
can_short = False
# Hyperopt 参数
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, load=True)
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.027, space="roi", optimize=True, load=True)
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.009, space="roi", optimize=True, load=True)
stoploss_param = DecimalParameter(low=-0.25, high=-0.05, default=-0.1, space="stoploss", optimize=True, load=True)
# 保护机制
protections = [
{"method": "StoplossGuard", "stop_duration": 60, "lookback_period": 120},
{"method": "MaxDrawdown", "lookback_period": 120, "max_allowed_drawdown": 0.05}
]
# FreqAI 配置
freqai_info = {
"model": "LightGBMRegressor",
"feature_parameters": {
"include_timeframes": ["5m", "15m", "1h"],
"include_corr_pairlist": [],
"label_period_candles": 12,
"include_shifted_candles": 3,
},
"data_split_parameters": {
"test_size": 0.2,
"shuffle": False,
"n_splits": 5 # 添加交叉验证
},
"model_training_parameters": {
"n_estimators": 200,
"learning_rate": 0.05,
"num_leaves": 10,
"min_child_weight": 1,
"verbose": -1,
},
}
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
if "close" not in dataframe.columns:
logger.error("数据框缺少必要的 'close' 列")
raise ValueError("数据框缺少必要的 'close' 列")
try:
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
dataframe["&-buy_rsi"] = (dataframe["close"].shift(-label_period) / dataframe["close"] - 1) * 100 # 修改目标为收益率
for col in ["&-buy_rsi", "%-volatility"]:
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
dataframe[col] = dataframe[col].ffill()
if dataframe[col].isna().any():
logger.warning(f"目标列 {col} 包含 NaN填充为 0")
dataframe[col] = dataframe[col].fillna(0)
logger.info(f"目标列 {col} 统计:\n{dataframe[col].describe().to_string()}")
except Exception as e:
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
raise
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
logger.info(f"处理交易对:{metadata['pair']}, 数据形状:{dataframe.shape}")
dataframe = self.freqai.start(dataframe, metadata, self)
# 计算传统指标
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
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["tema"] = ta.TEMA(dataframe, timeperiod=9)
# 检查FreqAI预测列
if "&-buy_rsi_pred" in dataframe.columns:
logger.info(f"&-buy_rsi_pred 统计:均值={dataframe['&-buy_rsi_pred'].mean():.2f}, 标准差={dataframe['&-buy_rsi_pred'].std():.2f}")
dataframe["buy_rsi_trend"] = np.where(
dataframe["&-buy_rsi_pred"] > dataframe["&-buy_rsi_pred"].shift(1), 1, 0
)
dataframe["&-sell_rsi_pred"] = dataframe["&-buy_rsi_pred"] + 30
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
dataframe["&-stoploss_pred"] = -0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)
dataframe["&-roi_0_pred"] = (dataframe["&-buy_rsi_pred"] / 1000).clip(0.01, 0.2)
for col in ["&-buy_rsi_pred", "&-sell_rsi_pred", "&-stoploss_pred", "&-roi_0_pred"]:
if dataframe[col].isna().any():
logger.warning(f"列 {col} 包含 NaN填充为默认值")
mean_value = dataframe[col].mean()
if pd.isna(mean_value):
mean_value = {
"&-buy_rsi_pred": 30,
"&-sell_rsi_pred": 70,
"&-stoploss_pred": -0.1,
"&-roi_0_pred": 0.05
}.get(col, 0)
dataframe[col] = dataframe[col].fillna(mean_value)
else:
logger.warning(f"&-buy_rsi_pred 列缺失,使用默认值初始化")
dataframe["buy_rsi_trend"] = 0
dataframe["&-buy_rsi_pred"] = 30
dataframe["&-sell_rsi_pred"] = 70
dataframe["&-stoploss_pred"] = -0.1
dataframe["&-roi_0_pred"] = 0.05
# 动态参数设置
try:
last_valid_idx = dataframe["&-stoploss_pred"].last_valid_index()
if last_valid_idx is None:
raise ValueError("没有有效的预测数据")
self.stoploss = float(np.clip(dataframe["&-stoploss_pred"].iloc[last_valid_idx], -0.25, -0.05))
self.buy_rsi.value = int(np.clip(dataframe["&-buy_rsi_pred"].iloc[last_valid_idx], 10, 50))
self.sell_rsi.value = int(np.clip(dataframe["&-sell_rsi_pred"].iloc[last_valid_idx], 50, 90))
self.roi_0.value = float(np.clip(dataframe["&-roi_0_pred"].iloc[last_valid_idx], 0.01, 0.2))
self.minimal_roi = {
0: self.roi_0.value,
15: self.roi_15.value,
30: self.roi_30.value,
60: 0.0
}
logger.info(f"动态参数设置: buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, stoploss={self.stoploss:.2%}")
except Exception as e:
logger.error(f"动态参数设置失败,使用默认值: {str(e)}")
self.stoploss = -0.1
self.buy_rsi.value = 27
self.sell_rsi.value = 59
self.minimal_roi = {0: 0.038, 15: 0.027, 30: 0.009, 60: 0.0}
dataframe = dataframe.replace([np.inf, -np.inf], 0)
dataframe = dataframe.ffill()
dataframe = dataframe.fillna(0)
logger.info(f"do_predict 分布:\n{dataframe['do_predict'].value_counts().to_string()}")
return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
enter_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["&-buy_rsi_pred"] - 5), # 放宽RSI条件
df["tema"] > df["tema"].shift(1),
df["volume"] > 0,
df["do_predict"] == 1
]
if enter_long_conditions:
condition_met = reduce(lambda x, y: x & y, enter_long_conditions)
df.loc[condition_met, ["enter_long", "enter_tag"]] = (1, "long")
if condition_met.any():
logger.info(f"买入信号触发:{metadata['pair']},时间={df.index[condition_met][-1]}")
else:
logger.debug(f"买入条件未满足:{metadata['pair']}do_predict={df['do_predict'].iloc[-1]}, rsi={df['rsi'].iloc[-1]:.2f}, buy_rsi_pred={df['&-buy_rsi_pred'].iloc[-1]:.2f}")
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
exit_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["&-sell_rsi_pred"]),
df["close"] < df["bb_lowerband"],
df["volume"] > 0,
df["do_predict"] == 1
]
time_exit = (df["date"] >= df["date"].shift(1) + pd.Timedelta(days=2))
df.loc[
(reduce(lambda x, y: x & y, exit_long_conditions)) | time_exit,
"exit_long"
] = 1
return df
```
```
```
```
```
```
```
```

View File

@ -1,69 +0,0 @@
{%set volume_pairlist = '{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 1800
}' %}
{
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": {{ max_open_trades }},
"stake_currency": "{{ stake_currency }}",
"stake_amount": {{ stake_amount }},
"tradable_balance_ratio": 0.99,
{{- ('\n "fiat_display_currency": "' + fiat_display_currency + '",') if fiat_display_currency else ''}}
{{- ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
"dry_run": {{ dry_run | lower }},
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": false,
"trading_mode": "{{ trading_mode }}",
"margin_mode": "{{ margin_mode }}",
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
{{ exchange | indent(4) }},
"pairlists": [
{{ volume_pairlist }}
],
"telegram": {
"enabled": {{ telegram | lower }},
"token": "{{ telegram_token }}",
"chat_id": "{{ telegram_chat_id }}"
},
"api_server": {
"enabled": {{ api_server | lower }},
"listen_ip_address": "{{ api_server_listen_addr | default("127.0.0.1", true) }}",
"listen_port": 8080,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "{{ api_server_jwt_key }}",
"ws_token": "{{ api_server_ws_token }}",
"CORS_origins": [],
"username": "{{ api_server_username }}",
"password": "{{ api_server_password }}"
},
"bot_name": "freqtrade",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@ -1,179 +0,0 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Dict, Optional, Union, Tuple
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import pandas_ta as pta
from technical import qtpylib
class {{ strategy }}(IStrategy):
"""
This is a strategy template to get you started.
More information in https://www.freqtrade.io/en/latest/strategy-customization/
You can:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy
- Add any lib you need to build your strategy
You must keep:
- the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep:
- timeframe, minimal_roi, stoploss, trailing_*
"""
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 3
# Optimal timeframe for the strategy.
timeframe = "5m"
# Can this strategy go short?
can_short: bool = False
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
minimal_roi = {
"60": 0.01,
"30": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy.
# This attribute will be overridden if the config file contains "stoploss".
stoploss = -0.10
# Trailing stoploss
trailing_stop = False
# trailing_only_offset_is_reached = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Run "populate_indicators()" only for new candle.
process_only_new_candles = True
# These values can be overridden in the config.
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 30
# Strategy parameters
buy_rsi = IntParameter(10, 40, default=30, space="buy")
sell_rsi = IntParameter(60, 90, default=70, space="sell")
{{- attributes | indent(4) }}
{{- plot_config | indent(4) }}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part
of the whitelist as well.
For more information, please consult the documentation
:return: List of tuples in the format (pair, interval)
Sample: return [("ETH/USDT", "5m"),
("BTC/USDT", "15m"),
]
"""
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
{{- indicators | indent(8) }}
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with entry columns populated
"""
dataframe.loc[
(
{{ buy_trend | indent(16) }}
(dataframe["volume"] > 0) # Make sure Volume is not 0
),
"enter_long"] = 1
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
"""
dataframe.loc[
(
{{ sell_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'enter_short'] = 1
"""
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit columns populated
"""
dataframe.loc[
(
{{ sell_trend | indent(16) }}
(dataframe["volume"] > 0) # Make sure Volume is not 0
),
"exit_long"] = 1
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
"""
dataframe.loc[
(
{{ buy_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_short'] = 1
"""
return dataframe
{{- additional_methods | indent(4) }}

View File

@ -1,419 +0,0 @@
13c13,14
< import time
---
>
>
31c32
< trailing_stop_positive = 0.02 # 2% for aggressive profit-taking
---
> trailing_stop_positive = 0.01
35c36
< atr_multiplier = DecimalParameter(1.0, 3.0, default=2.5, space='sell') # 2.5 for lenient stop-loss
---
> atr_multiplier = DecimalParameter(1.0, 3.0, default=2.0, space='sell')
50,51c51,52
< "include_shifted_candles": 2,
< "principal_component_analysis": True
---
> "include_shifted_candles": 2, # 添加历史偏移特征
> "principal_component_analysis": True # 启用 PCA
60c61
< "purge_old_models": True
---
> "purge_old_models": True # 清理旧模型
64a66
> # 初始化特征缓存
66c68
< self.order_book_cache = {} # Cache for order book data
---
>
72,81d73
< # Ensure DatetimeIndex
< if not isinstance(dataframe.index, pd.DatetimeIndex):
< logger.warning("Index is not DatetimeIndex in feature_engineering_expand_all")
< if 'date' in dataframe.columns:
< dataframe.set_index(pd.to_datetime(dataframe['date']), inplace=True)
< dataframe.drop(columns=['date'], inplace=True)
< else:
< logger.error("No 'date' column and index is not DatetimeIndex")
< raise ValueError("Invalid DataFrame index")
<
102c94,95
< # Order book imbalance for BTC/USDT and ETH/USDT
---
> # 仅为 BTC/USDT 和 ETH/USDT 生成 order_book_imbalance
> #
112c105
< dataframe[f"%-%-order_book_imbalance"] = 0.0
---
> dataframe[f"%-%-order_book_imbalance"] = 0.0
128c121
< # Ensure DatetimeIndex
---
> # 确保索引是 DatetimeIndex
130,136c123
< logger.warning("Index is not DatetimeIndex in feature_engineering_standard")
< if 'date' in dataframe.columns:
< dataframe.set_index(pd.to_datetime(dataframe['date']), inplace=True)
< dataframe.drop(columns=['date'], inplace=True)
< else:
< logger.error("No 'date' column and index is not DatetimeIndex")
< raise ValueError("Invalid DataFrame index")
---
> dataframe = dataframe.set_index(pd.DatetimeIndex(dataframe.index))
153a141,142
> 输入dataframeK线数据close, high, lowmetadata交易对信息configFreqAI配置
> 输出更新后的dataframe包含目标标签
155,161c144,146
< # Ensure DatetimeIndex
< if not isinstance(dataframe.index, pd.DatetimeIndex):
< logger.error("Invalid index in set_freqai_targets")
< raise ValueError("DataFrame index must be DatetimeIndex")
<
< label_period = self.freqai_config["feature_parameters"]["label_period_candles"]
< pair = metadata["pair"]
---
> # 获取配置参数
> label_period = self.freqai_config["feature_parameters"]["label_period_candles"] # 标签预测周期如5分钟K线的N根
> pair = metadata["pair"] # 当前交易对如DOGE/USDT
163c148
< # 计算未来价格变化率
---
> # 计算未来价格变化率(现有逻辑)
166c151
< # 计算不同时间窗口的ROI
---
> # 计算不同时间窗口的ROI现有逻辑
168c153
< candles = int(minutes / 5)
---
> candles = int(minutes / 5) # 假设5分钟K线
174c159
< # 计算ADX
---
> # 计算市场状态指标ADX14周期与label_period_candles对齐
177c162
< # 币对特定的ADX阈值和止损/ROI范围
---
> # 定义币对特定的ADX阈值和止损/ROI范围
180,187c165,172
< "adx_trend": 25,
< "adx_oscillation": 15,
< "stoploss_trend": -0.10,
< "stoploss_oscillation": -0.06,
< "stoploss_mid": -0.08,
< "roi_trend": 0.10,
< "roi_oscillation": 0.04,
< "roi_mid": 0.07
---
> "adx_trend": 20, # 趋势市场ADX阈值
> "adx_oscillation": 15, # 震荡市场ADX阈值
> "stoploss_trend": -0.08, # 趋势市场止损:-8%
> "stoploss_oscillation": -0.04, # 震荡市场止损:-4%
> "stoploss_mid": -0.06, # 中间状态止损:-6%
> "roi_trend": 0.06, # 趋势市场ROI6%
> "roi_oscillation": 0.025, # 震荡市场ROI2.5%
> "roi_mid": 0.04 # 中间状态ROI4%
191,197c176,182
< "adx_oscillation": 15,
< "stoploss_trend": -0.05,
< "stoploss_oscillation": -0.025,
< "stoploss_mid": -0.035,
< "roi_trend": 0.05,
< "roi_oscillation": 0.025,
< "roi_mid": 0.035
---
> "adx_oscillation": 20,
> "stoploss_trend": -0.03,
> "stoploss_oscillation": -0.015,
> "stoploss_mid": -0.02,
> "roi_trend": 0.03,
> "roi_oscillation": 0.015,
> "roi_mid": 0.02
200,207c185,192
< "adx_trend": 25,
< "adx_oscillation": 15,
< "stoploss_trend": -0.08,
< "stoploss_oscillation": -0.04,
< "stoploss_mid": -0.06,
< "roi_trend": 0.08,
< "roi_oscillation": 0.035,
< "roi_mid": 0.055
---
> "adx_trend": 22,
> "adx_oscillation": 18,
> "stoploss_trend": -0.06,
> "stoploss_oscillation": -0.03,
> "stoploss_mid": -0.045,
> "roi_trend": 0.045,
> "roi_oscillation": 0.02,
> "roi_mid": 0.03
210,237c195,202
< "adx_trend": 25,
< "adx_oscillation": 15,
< "stoploss_trend": -0.07,
< "stoploss_oscillation": -0.035,
< "stoploss_mid": -0.05,
< "roi_trend": 0.07,
< "roi_oscillation": 0.03,
< "roi_mid": 0.05
< },
< "OKB/USDT": {
< "adx_trend": 25,
< "adx_oscillation": 15,
< "stoploss_trend": -0.10,
< "stoploss_oscillation": -0.06,
< "stoploss_mid": -0.08,
< "roi_trend": 0.10,
< "roi_oscillation": 0.04,
< "roi_mid": 0.07
< },
< "TON/USDT": {
< "adx_trend": 25,
< "adx_oscillation": 15,
< "stoploss_trend": -0.07,
< "stoploss_oscillation": -0.04,
< "stoploss_mid": -0.055,
< "roi_trend": 0.07,
< "roi_oscillation": 0.03,
< "roi_mid": 0.05
---
> "adx_trend": 22,
> "adx_oscillation": 18,
> "stoploss_trend": -0.06,
> "stoploss_oscillation": -0.03,
> "stoploss_mid": -0.045,
> "roi_trend": 0.045,
> "roi_oscillation": 0.02,
> "roi_mid": 0.03
241c206
< # 动态止损
---
> # 动态化 &-stoploss_pred基于市场状态和币对
248,255c213,221
< if adx_value > thresholds["adx_trend"]:
< dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"]
< elif adx_value < thresholds["adx_oscillation"]:
< dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"]
< else:
< dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"]
< if dataframe.at[index, "&-stoploss_pred"] < -0.12:
< dataframe.at[index, "&-stoploss_pred"] = -0.12
---
> if adx_value > thresholds["adx_trend"]: # 趋势市场
> dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"] # 宽松止损
> elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
> dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"] # 严格止损
> else: # 中间状态
> dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"] # 中等止损
> # 风险控制:设置止损下限
> if dataframe.at[index, "&-stoploss_pred"] < -0.10:
> dataframe.at[index, "&-stoploss_pred"] = -0.10
257c223
< # 动态ROI
---
> # 动态化 &-roi_0_pred基于市场趋势和币对
264,271c230,238
< if adx_value > thresholds["adx_trend"]:
< dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"]
< elif adx_value < thresholds["adx_oscillation"]:
< dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"]
< else:
< dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"]
< if dataframe.at[index, "&-roi_0_pred"] > 0.15:
< dataframe.at[index, "&-roi_0_pred"] = 0.15
---
> if adx_value > thresholds["adx_trend"]: # 强趋势市场
> dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"] # 高ROI
> elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
> dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"] # 低ROI
> else: # 中间状态
> dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"] # 中等ROI
> # 风险控制设置ROI上限
> if dataframe.at[index, "&-roi_0_pred"] > 0.10:
> dataframe.at[index, "&-roi_0_pred"] = 0.10
273c240
< # RSI预测
---
> # 计算RSI预测现有逻辑
278c245
< dataframe = dataframe.ffill().fillna(0)
---
> dataframe = dataframe.fillna(method="ffill").fillna(0)
295,304d261
< # Ensure DatetimeIndex before FreqAI
< if not isinstance(dataframe.index, pd.DatetimeIndex):
< logger.warning("Index is not DatetimeIndex in populate_indicators")
< if 'date' in dataframe.columns:
< dataframe.set_index(pd.to_datetime(dataframe['date']), inplace=True)
< dataframe.drop(columns=['date'], inplace=True)
< else:
< logger.error("No 'date' column and index is not DatetimeIndex")
< raise ValueError("Invalid DataFrame index")
<
306d262
< logger.debug(f"DataFrame columns before FreqAI: {list(dataframe.columns)}")
314,321c270,273
< # 预测统计
< if "&-s_close" in dataframe.columns:
< logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, "
< f"方差={dataframe['&-s_close'].var():.4f}")
<
< # 添加ATR指标
< atr_col = f'ATR_{self.atr_period.value}'
< dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
---
> # 预测统计
> if "&-s_close" in dataframe.columns:
> logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, "
> f"方差={dataframe['&-s_close'].var():.4f}")
323,324c275,276
< logger.debug(f"生成的列:{list(dataframe.columns)}")
< return dataframe
---
> logger.debug(f"生成的列:{list(dataframe.columns)}")
> return dataframe
330a283,289
> # 确保返回 DataFrame防止 None
> if dataframe is None:
> dataframe = DataFrame()
> dataframe['ATR_{}'.format(self.atr_period.value)] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
> return dataframe
>
>
334a294
> # 确保 "%-%-rsi-14" 列存在
340,342c300,302
< (dataframe["&-s_close"] > 0.01) &
< (dataframe["do_predict"] == 1) &
< (dataframe["%-%-rsi-14"] < dataframe["&-buy_rsi_pred"])
---
> (dataframe["&-s_close"] > 0.01) & # 预测价格上涨 > 1%
> (dataframe["do_predict"] == 1) & # 预测可靠
> (dataframe["%-%-rsi-14"] < dataframe["&-buy_rsi_pred"]) # RSI 低于动态阈值
345a306
> # 设置 entry_price 列,用于止损逻辑
350,351c311
<
< def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.5) -> DataFrame:
---
> def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
353a314,318
> :param dataframe: 原始DataFrame
> :param metadata: 策略元数据
> :param atr_col: 使用的ATR列名
> :param multiplier: ATR乘数
> :return: 更新后的DataFrame
360c325,326
< 'exit_long'] = 1
---
> 'exit_long'
> ] = 1
363d328
<
365,373c330,332
< """
< 基于动态止损和ROI预测生成退出信号。
< """
< atr_col = f'ATR_{self.atr_period.value}'
< if atr_col not in dataframe.columns:
< dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
<
< dataframe['stop_loss_line'] = dataframe['entry_price'] - (dataframe[atr_col] * self.atr_multiplier.value)
< dataframe['roi_target'] = dataframe['entry_price'] * (1 + dataframe['&-roi_0_pred'])
---
> # 确保 ATR 列存在
> if 'ATR_14' not in dataframe.columns:
> dataframe['ATR_14'] = 0.0
375,380c334,335
< dataframe.loc[
< (
< (dataframe['close'] <= dataframe['stop_loss_line']) |
< (dataframe['close'] >= dataframe['roi_target'])
< ),
< 'exit_long'] = 1
---
> # 计算动态止损线
> dataframe['stop_loss_line'] = dataframe['entry_price'] - (dataframe['ATR_14'] * 2)
382,383c337,338
< if dataframe['exit_long'].iloc[-1] == 1:
< self.dp.send_msg(f"ATR: {dataframe[atr_col].iloc[-1]:.5f}, Stop Loss Line: {dataframe['stop_loss_line'].iloc[-1]:.5f}, ROI Target: {dataframe['roi_target'].iloc[-1]:.5f}")
---
> # 发送止损信息
> self.dp.send_msg(f"ATR: {dataframe['ATR_14'].iloc[-1]:.5f}, Stop Loss Line: {dataframe['stop_loss_line'].iloc[-1]:.5f}")
385,386c340,341
< logger.debug(f"生成 {dataframe['exit_long'].sum()} 个退出信号")
< return dataframe
---
> # 应用动态止损逻辑
> return self._dynamic_stop_loss(dataframe, metadata)
397d351
<
401,403c355
< """
< 自定义静态止损基于ATR。
< """
---
>
405,408c357,359
< atr_col = f'ATR_{self.atr_period.value}'
< atr_value = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)[atr_col].iloc[-1]
< trailing_stop = current_rate - atr_value * 2.0
< return trailing_stop / current_rate - 1
---
> atr_value = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)['ATR_14'].iloc[-1]
> trailing_stop = current_rate - atr_value * 1.5
> return trailing_stop / current_rate - 1 # 返回相对百分比
432a384
> # 假设数据总是新鲜的(用于测试)
437c389
< 获取 OKX 订单簿数据,带重试和缓存。
---
> 获取 OKX 订单簿数据。
439,464c391,399
< cache_key = f"{pair}_{limit}"
< cache_timeout = 60 # Cache for 60 seconds
< if cache_key in self.order_book_cache:
< cached_time, cached_data = self.order_book_cache[cache_key]
< if time.time() - cached_time < cache_timeout:
< logger.debug(f"Using cached order book for {pair}")
< return cached_data
<
< max_retries = 3
< for attempt in range(max_retries):
< try:
< exchange = ccxt.okx(self.config["exchange"]["ccxt_config"])
< order_book = exchange.fetch_order_book(pair, limit=limit)
< bids = sum([bid[1] for bid in order_book["bids"]])
< asks = sum([ask[1] for ask in order_book["asks"]])
< result = {"bids": bids, "asks": asks}
< # Cache result
< self.order_book_cache[cache_key] = (time.time(), result)
< return result
< except Exception as e:
< logger.warning(f"Attempt {attempt + 1}/{max_retries} failed for {pair}: {str(e)}")
< if attempt < max_retries - 1:
< time.sleep(1) # Wait before retry
< else:
< logger.error(f"获取 {pair} 订单簿失败 after {max_retries} attempts: {str(e)}")
< return {"bids": 0, "asks": 0}
---
> try:
> exchange = ccxt.okx(self.config["exchange"]["ccxt_config"])
> order_book = exchange.fetch_order_book(pair, limit=limit)
> bids = sum([bid[1] for bid in order_book["bids"]])
> asks = sum([ask[1] for ask in order_book["asks"]])
> return {"bids": bids, "asks": asks}
> except Exception as e:
> logger.error(f"获取 {pair} 订单簿失败:{str(e)}")
> return {"bids": 0, "asks": 0}
470a406
> # 初始化模型
475a412
> # 调用 FreqAI 训练
477a415
> # 记录训练集性能
483a422
> # 记录测试集性能(如果可用)
490a430
> # 特征重要性

View File

@ -1,584 +0,0 @@
import logging
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import pandas as pd
import numpy as np
import talib as ta
import datetime
from typing import Dict, List, Optional
from sklearn.metrics import mean_squared_error
from freqtrade.strategy import CategoricalParameter, DecimalParameter
from xgboost import XGBRegressor
import ccxt
logger = logging.getLogger(__name__)
class OKXRegressionStrategy(IStrategy):
"""
Freqtrade AI 策略使用回归模型进行 OKX 数据上的仅做多交易
- 数据通过 CCXT OKX 交易所获取
- 使用 XGBoost 回归模型预测价格变化
- 仅生成做多买入信号不做空
- 适配 Freqtrade 2025.3继承 IStrategy
"""
# 指标所需的最大启动蜡烛数
startup_candle_count: int = 20
# 策略元数据(建议通过 config.json 配置)
trailing_stop = True
trailing_stop_positive = 0.07
max_open_trades = 3
stake_amount = 'dynamic'
atr_period = CategoricalParameter([7, 14, 21], default=14, space='buy')
atr_multiplier = DecimalParameter(1.0, 3.0, default=2.0, space='sell')
# FreqAI 配置
freqai_config = {
"enabled": True,
"identifier": "okx_regression_v1",
"model_training_parameters": {
"n_estimators": 100,
"learning_rate": 0.05,
"max_depth": 6
},
"feature_parameters": {
"include_timeframes": ["5m", "15m", "1h"],
"include_corr_pairlist": ["BTC/USDT", "ETH/USDT"],
"label_period_candles": 12,
"include_shifted_candles": 2, # 添加历史偏移特征
"principal_component_analysis": True # 启用 PCA
},
"data_split_parameters": {
"test_size": 0.2,
"random_state": 42,
"shuffle": False
},
"train_period_days": 90,
"backtest_period_days": 30,
"purge_old_models": True # 清理旧模型
}
def __init__(self, config: Dict):
super().__init__(config)
# 初始化特征缓存
self.feature_cache = {}
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: Dict, **kwargs) -> DataFrame:
"""
为每个时间框架和相关交易对生成特征
"""
cache_key = f"{metadata.get('pair', 'unknown')}_{period}"
if cache_key in self.feature_cache:
logger.debug(f"使用缓存特征:{cache_key}")
return self.feature_cache[cache_key]
# RSI
dataframe[f"%-%-rsi-{period}"] = ta.RSI(dataframe["close"], timeperiod=period)
# MACD
macd, macdsignal, _ = ta.MACD(dataframe["close"], fastperiod=12, slowperiod=26, signalperiod=9)
dataframe[f"%-%-macd-{period}"] = macd
dataframe[f"%-%-macdsignal-{period}"] = macdsignal
# 布林带宽度
upper, middle, lower = ta.BBANDS(dataframe["close"], timeperiod=period)
dataframe[f"%-%-bb_width-{period}"] = (upper - lower) / middle
# 成交量均线
dataframe[f"%-%-volume_ma-{period}"] = ta.SMA(dataframe["volume"], timeperiod=period)
# 仅为 BTC/USDT 和 ETH/USDT 生成 order_book_imbalance
#
pair = metadata.get('pair', 'unknown')
# 注释掉订单簿相关代码
# if pair in ["BTC/USDT", "ETH/USDT"]:
# try:
# order_book = self.fetch_okx_order_book(pair)
# dataframe[f"%-%-order_book_imbalance"] = (
# order_book["bids"] - order_book["asks"]
# ) / (order_book["bids"] + order_book["asks"] + 1e-10)
# except Exception as e:
# logger.warning(f"Failed to fetch order book for {pair}: {str(e)}")
# dataframe[f"%-%-order_book_imbalance"] = 0.0
# 数据清洗
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
dataframe = dataframe.ffill().fillna(0)
# 缓存特征
self.feature_cache[cache_key] = dataframe.copy()
logger.debug(f"周期 {period} 特征:{list(dataframe.filter(like='%-%-').columns)}")
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
"""
添加基础时间框架的全局特征
"""
# 确保索引是 DatetimeIndex
if not isinstance(dataframe.index, pd.DatetimeIndex):
dataframe = dataframe.set_index(pd.DatetimeIndex(dataframe.index))
# 价格变化率
dataframe["%-price_change"] = dataframe["close"].pct_change()
# 时间特征:小时
dataframe["%-hour_of_day"] = dataframe.index.hour / 24.0
# 数据清洗
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
dataframe = dataframe.ffill().fillna(0)
logger.debug(f"全局特征:{list(dataframe.filter(like='%-%-').columns)}")
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
"""
设置回归模型的目标变量为不同币对设置动态止损和ROI阈值
输入dataframeK线数据close, high, lowmetadata交易对信息configFreqAI配置
输出更新后的dataframe包含目标标签
"""
# 获取配置参数
label_period = self.freqai_config["feature_parameters"]["label_period_candles"] # 标签预测周期如5分钟K线的N根
pair = metadata["pair"] # 当前交易对如DOGE/USDT
# 计算未来价格变化率(现有逻辑)
dataframe["&-s_close"] = (dataframe["close"].shift(-label_period) - dataframe["close"]) / dataframe["close"]
# 计算不同时间窗口的ROI现有逻辑
for minutes in [0, 15, 30]:
candles = int(minutes / 5) # 假设5分钟K线
if candles > 0:
dataframe[f"&-roi_{minutes}"] = (dataframe["close"].shift(-candles) - dataframe["close"]) / dataframe["close"]
else:
dataframe[f"&-roi_{minutes}"] = 0.0
# 计算市场状态指标ADX14周期与label_period_candles对齐
dataframe["adx"] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
# 定义币对特定的ADX阈值和止损/ROI范围
pair_thresholds = {
"BTC/USDT": {
"adx_trend": 25,
"adx_oscillation": 20,
"stoploss_trend": -0.03,
"stoploss_oscillation": -0.015,
"stoploss_mid": -0.02,
"roi_trend": 0.03,
"roi_oscillation": 0.015,
"roi_mid": 0.02
},
"SOL/USDT": {
"adx_trend": 22,
"adx_oscillation": 18,
"stoploss_trend": -0.06,
"stoploss_oscillation": -0.03,
"stoploss_mid": -0.045,
"roi_trend": 0.045,
"roi_oscillation": 0.02,
"roi_mid": 0.03
},
"XRP/USDT": {
"adx_trend": 22,
"adx_oscillation": 18,
"stoploss_trend": -0.06,
"stoploss_oscillation": -0.03,
"stoploss_mid": -0.045,
"roi_trend": 0.045,
"roi_oscillation": 0.02,
"roi_mid": 0.03
},
"XRP/USDT": {
"adx_trend": 22,
"adx_oscillation": 18,
"stoploss_trend": -0.06,
"stoploss_oscillation": -0.03,
"stoploss_mid": -0.045,
"roi_trend": 0.045,
"roi_oscillation": 0.02,
"roi_mid": 0.03
},
"OKB/USDT": {
"adx_trend": 36, # 放松趋势识别,更低的 ADX 阈值 (原值 x2)
"adx_oscillation": 24, # 更低的震荡识别阈值 (原值 x2)
"stoploss_trend": -0.20, # 更宽松的趋势止损 (原值 x2)
"stoploss_oscillation": -0.12, # 更宽松的震荡止损 (原值 x2)
"stoploss_mid": -0.16, # 中间状态止损也放宽 (原值 x2)
"roi_trend": 0.14, # 提高趋势 ROI 目标 (原值 x2)
"roi_oscillation": 0.08, # 提高震荡 ROI 目标 (原值 x2)
"roi_mid": 0.10 # 中间状态 ROI 适度提高 (原值 x2)
}
}
# 动态化 &-stoploss_pred基于市场状态和币对
dataframe["&-stoploss_pred"] = 0.0
for index, row in dataframe.iterrows():
thresholds = pair_thresholds.get(pair, {})
if not thresholds:
continue
adx_value = row["adx"]
if adx_value > thresholds["adx_trend"]: # 趋势市场
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"] # 宽松止损
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"] # 严格止损
else: # 中间状态
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"] # 中等止损
# 风险控制:设置止损下限
if dataframe.at[index, "&-stoploss_pred"] < -0.10:
dataframe.at[index, "&-stoploss_pred"] = -0.10
# 动态化 &-roi_0_pred基于市场趋势和币对
dataframe["&-roi_0_pred"] = 0.0
for index, row in dataframe.iterrows():
thresholds = pair_thresholds.get(pair, {})
if not thresholds:
continue
adx_value = row["adx"]
if adx_value > thresholds["adx_trend"]: # 强趋势市场
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"] # 高ROI
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"] # 低ROI
else: # 中间状态
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"] # 中等ROI
# 风险控制设置ROI上限
if dataframe.at[index, "&-roi_0_pred"] > 0.10:
dataframe.at[index, "&-roi_0_pred"] = 0.10
# 计算RSI预测现有逻辑
dataframe["&-buy_rsi_pred"] = ta.RSI(dataframe["close"], timeperiod=14).rolling(20).mean()
# 数据清洗
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
#dataframe = dataframe.fillna(method="ffill").fillna(0)
dataframe = dataframe.ffill()
# 验证目标
required_targets = ["&-s_close", "&-roi_0", "&-buy_rsi_pred", "&-stoploss_pred", "&-roi_0_pred"]
missing_targets = [col for col in required_targets if col not in dataframe.columns]
if missing_targets:
logger.error(f"缺少目标列:{missing_targets}")
raise ValueError(f"目标初始化失败:{missing_targets}")
logger.debug(f"目标初始化完成。DataFrame 形状:{dataframe.shape}")
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
"""
使用 FreqAI 生成指标和预测
"""
try:
logger.debug(f"FreqAI 对象:{type(self.freqai)}")
dataframe = self.freqai.start(dataframe, metadata, self)
# 验证数据完整性
if dataframe["close"].isna().any() or dataframe["volume"].isna().any():
logger.warning("检测到 OKX 数据缺失,使用前向填充")
dataframe = dataframe.ffill().fillna(0)
# 预测统计
if "&-s_close" in dataframe.columns:
logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, "
f"方差={dataframe['&-s_close'].var():.4f}")
logger.debug(f"生成的列:{list(dataframe.columns)}")
return dataframe
except Exception as e:
logger.error(f"FreqAI start 失败:{str(e)}")
raise
finally:
logger.debug("populate_indicators 完成")
# 确保返回 DataFrame防止 None
if dataframe is None:
dataframe = DataFrame()
dataframe['ATR_{}'.format(self.atr_period.value)] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
# 添加 RSI 和布林带指标
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
upper, middle, lower = ta.BBANDS(dataframe['close'], timeperiod=20, nbdevup=2, nbdevdn=2)
dataframe['bb_upper'] = upper
dataframe['bb_middle'] = middle
dataframe['bb_lower'] = lower
# 添加高时间框架数据(例如 1h
dataframe_1h = None # 显式声明
if self.config['timeframe'] != '1h':
dataframe_1h = self.dp.get_analyzed_dataframe(metadata['pair'], '1h')[0]
# 确保 dataframe_1h 存在 'close' 列并且非空
if dataframe_1h is not None and not dataframe_1h.empty and 'close' in dataframe_1h.columns:
dataframe['trend_1h'] = dataframe_1h['close'].rolling(window=20).mean()
else:
# 回退到当前时间框架数据
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
else:
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
# 记录 1h 数据框信息
logger.debug(f"dataframe_1h columns: {dataframe_1h.columns.tolist() if dataframe_1h is not None and not dataframe_1h.empty else '未使用'}")
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
"""
使用模型预测置信度进行入场过滤
- 仅当 `pred_upper - pred_lower > 0.02` 时允许开仓
"""
if "&-s_close" in dataframe.columns and "pred_upper" in dataframe.columns and "pred_lower" in dataframe.columns:
# 计算置信区间宽度
dataframe["confidence_width"] = dataframe["pred_upper"] - dataframe["pred_lower"]
# 应用置信度过滤(仅在宽度 > 0.02 时允许交易)
dataframe.loc[
(dataframe["confidence_width"] > 0.02),
"enter_long"
] = 1
return dataframe
# 已废弃:使用 custom_stoploss + ATR 百分位替代
# def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
# ...
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
# 根据波动率百分位调整止盈倍数
if percentile > 80: # 高波动市场,缩短止盈距离
volatility_adjustment = 0.8
elif percentile < 20: # 低波动市场,拉长止盈距离
volatility_adjustment = 1.2
else: # 正常波动市场,保持默认
volatility_adjustment = 1.0
# 获取ADX指标判断趋势强度
dataframe['adx'] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
adx_value = dataframe['adx'].iloc[-1]
# 根据ADX趋势强度调整止盈倍数
if adx_value > 25: # 强趋势,延长止盈距离
trend_adjustment = 1.3
elif adx_value < 15: # 震荡行情,缩短止盈距离
trend_adjustment = 0.7
else: # 中性趋势,保持默认
trend_adjustment = 1.0
# 综合调整止盈倍数
adjusted_multiplier = take_profit_multiplier * volatility_adjustment * trend_adjustment
# 计算止盈线
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe[atr_col] * adjusted_multiplier
# 应用止盈逻辑
dataframe.loc[
(dataframe['close'] > dataframe['take_profit_line']),
'exit_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
"""
当前退出逻辑已由 custom_stoploss callback_stop_loss 管理
此方法保留为空以维持向后兼容性
"""
return dataframe
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], **kwargs) -> float:
"""
动态下注每笔交易占账户余额 2%
"""
balance = self.wallets.get_available_stake_amount()
stake = balance * 0.02
return min(max(stake, min_stake), max_stake)
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, profit_percent: float,
after_fill: bool, **kwargs) -> Optional[float]:
"""
自适应止损基于市场波动率百分位动态调整ATR乘数
"""
if trade.enter_tag == 'long':
# 获取多个周期的ATR值
dataframe = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)
# 计算不同周期的ATR
dataframe['ATR_7'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=7)
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
dataframe['ATR_21'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=21)
# 计算20日平均ATR用于百分位计算
avg_atr_window = 20
dataframe['avg_atr'] = dataframe['ATR_14'].rolling(window=avg_atr_window).mean()
# 根据交易对调整基础ATR值
pair_specific_atr = {
"BTC/USDT": latest_row['ATR_14'],
"ETH/USDT": latest_row['ATR_14'],
"OKB/USDT": latest_row['ATR_14'], # 使用更长周期 ATR 减少波动影响
"TON/USDT": latest_row['ATR_7']
}
current_atr = latest_row['avg_atr']
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
# 根据市场波动率百分位选择ATR乘数
if percentile > 80: # 高波动市场
atr_multiplier = 1.5
elif percentile < 20: # 低波动市场
atr_multiplier = 2.5
else: # 正常波动市场
atr_multiplier = 2.0
# 根据交易对调整基础ATR值
pair_specific_atr = {
"BTC/USDT": latest_row['ATR_14'],
"ETH/USDT": latest_row['ATR_14'],
"OKB/USDT": latest_row['ATR_14'], # 使用更长周期 ATR 减少波动影响
"TON/USDT": latest_row['ATR_7']
}
if pair in pair_specific_atr:
base_atr = pair_specific_atr[pair]
else:
base_atr = latest_row['ATR_14']
# 计算追踪止损价格
trailing_stop = current_rate - base_atr * atr_multiplier
# 添加额外条件:确保止损不低于入场价的一定比例
min_profit_ratio = 0.005 # 最低盈利0.5%
min_stop_price = trade.open_rate * (1 + min_profit_ratio)
final_stop_price = max(trailing_stop, min_stop_price)
return final_stop_price / current_rate - 1 # 返回相对百分比
return None
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
禁用杠杆仅做多
"""
return 1.0
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
"""
入场确认逻辑已被禁用以简化流程
"""
return True
def check_data_freshness(self, pair: str, current_time: 'datetime') -> bool:
"""
数据新鲜度检查已被禁用以简化逻辑
"""
return True
def fit(self, data_dictionary: Dict, metadata: Dict, **kwargs) -> None:
"""
训练回归模型并记录性能
"""
try:
# 初始化模型
if not hasattr(self, 'model') or self.model is None:
model_params = self.freqai_config["model_training_parameters"]
self.model = XGBRegressor(**model_params)
logger.debug("初始化新的 XGBoost 回归模型")
# 调用 FreqAI 训练
self.freqai.fit(data_dictionary, metadata, **kwargs)
# 记录训练集性能
train_data = data_dictionary["train_features"]
train_labels = data_dictionary["train_labels"]
train_predictions = self.model.predict(train_data)
train_mse = mean_squared_error(train_labels, train_predictions)
logger.info(f"训练集 MSE{train_mse:.6f}")
# 记录测试集性能(如果可用)
if "test_features" in data_dictionary:
test_data = data_dictionary["test_features"]
test_labels = data_dictionary["test_labels"]
test_predictions = self.model.predict(test_data)
test_mse = mean_squared_error(test_labels, test_predictions)
logger.info(f"测试集 MSE{test_mse:.6f}")
# 特征重要性
if hasattr(self.model, 'feature_importances_'):
importance = self.model.feature_importances_
logger.debug(f"特征重要性:{dict(zip(train_data.columns, importance))}")
except Exception as e:
logger.error(f"FreqAI fit 失败:{str(e)}")
raise
def _callback_stop_loss(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
动态回调止损逻辑基于ATR调整回撤阈值并结合RSI和布林带过滤信号
"""
pair = metadata.get('pair', 'unknown')
# 设置默认参数
atr_col = 'ATR_14'
rolling_high_period = 20
rsi_overbought = 70
# 设置不同币种的回调乘数
callback_multipliers = {
"BTC/USDT": 1.5,
"ETH/USDT": 2.0,
"OKB/USDT": 1.3,
"TON/USDT": 2.0,
}
callback_multiplier = callback_multipliers.get(pair, 2.0)
# 确保ATR列存在
if atr_col not in dataframe.columns:
dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
# 计算动态回调百分比基于ATR
dataframe['callback_threshold'] = dataframe[atr_col] * callback_multiplier
# 计算ATR
if 'ATR_14' not in dataframe.columns:
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
# 计算最近高点
dataframe['rolling_high'] = dataframe['close'].rolling(window=20).max()
# 计算回调阈值
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe['ATR_14'] * callback_multiplier
# 应用止盈逻辑
dataframe.loc[
(dataframe['close'] > dataframe['take_profit_line']),
'exit_long'
] = 1
# 计算当前价格相对于最近高点的回撤比例使用ATR标准化
dataframe['callback_ratio'] = (dataframe['close'] - dataframe['rolling_high']) / dataframe['rolling_high']
dataframe['callback_condition_atr'] = (dataframe['close'] - dataframe['rolling_high']) <= -dataframe['callback_threshold']
# 获取RSI和布林带信息
dataframe['in_overbought'] = dataframe['rsi'] > rsi_overbought
dataframe['below_bb_upper'] = dataframe['close'] < dataframe['bb_upper']
# 获取高时间框架趋势1小时均线
dataframe['trend_up'] = dataframe['close'] > dataframe['trend_1h']
dataframe['trend_down'] = dataframe['close'] < dataframe['trend_1h']
# 综合回调止损条件
callback_condition = (
dataframe['callback_condition_atr'] &
((dataframe['in_overbought'] | (~dataframe['below_bb_upper']))) &
dataframe['trend_down']
)
# 应用回调止损逻辑
dataframe.loc[callback_condition, 'exit_long'] = 1
return dataframe

View File

@ -1,69 +0,0 @@
from freqtrade.strategy.interface import IStrategy
import pandas as pd
import numpy as np
import talib as ta
import logging
import datetime
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
logger = logging.getLogger(__name__)
class MyOptimizedStrategy(IStrategy):
# --- Hyperoptables ---
buy_rsi = IntParameter(20, 50, default=30, space="buy")
sell_rsi = IntParameter(50, 80, default=70, space="sell")
# --- FreqAI 相关 ---
minimal_roi = {"0": 0.05, "30": 0.02, "60": 0}
stoploss = -0.1
trailing_stop = True
trailing_stop_positive = 0.03
trailing_stop_positive_offset = 0.05
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["up_or_down"] = np.where(
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
# 添加 volume_ma
dataframe["volume_ma"] = dataframe["volume"].rolling(window=20).mean()
# 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.replace([np.inf, -np.inf], 0, inplace=True)
dataframe.ffill(inplace=True)
dataframe.fillna(0, inplace=True)
return dataframe
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
conditions = [
(dataframe['do_predict'] == 1),
(dataframe['rsi'] < dataframe['&-buy_rsi_pred']),
(dataframe['close'] < dataframe['bb_lowerband'])
]
dataframe.loc[reduce(np.logical_and, conditions), 'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
conditions = [
(dataframe['do_predict'] == 1),
(dataframe['rsi'] > dataframe['&-sell_rsi_pred'])
]
dataframe.loc[reduce(np.logical_and, 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:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_row = dataframe.iloc[-1]
if "&-stoploss" in last_row:
return float(last_row["&-stoploss"])
return self.stoploss

View File

@ -1,57 +0,0 @@
from datetime import datetime
from math import exp
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.optimize.hyperopt import IHyperOptLoss
# Define some constants:
# set TARGET_TRADES to suit your number concurrent trades so its realistic
# to the number of days
TARGET_TRADES = 600
# This is assumed to be expected avg profit * expected trade count.
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
# self.expected_max_profit = 3.85
# Check that the reported Σ% values do not exceed this!
# Note, this is ratio. 3.85 stated above means 385Σ%.
EXPECTED_MAX_PROFIT = 3.0
# max average trade duration in minutes
# if eval ends with higher value, we consider it a failed eval
MAX_ACCEPTED_TRADE_DURATION = 300
class SampleHyperOptLoss(IHyperOptLoss):
"""
Defines the default loss function for hyperopt
This is intended to give you some inspiration for your own loss function.
The Function needs to return a number (float) - which becomes smaller for better backtest
results.
"""
@staticmethod
def hyperopt_loss_function(
results: DataFrame,
trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
processed: dict[str, DataFrame],
*args,
**kwargs,
) -> float:
"""
Objective function, returns smaller number for better results
"""
total_profit = results["profit_ratio"].sum()
trade_duration = results["trade_duration"].mean()
trade_loss = 1 - 0.25 * exp(-((trade_count - TARGET_TRADES) ** 2) / 10**5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
result = trade_loss + profit_loss + duration_loss
return result

View File

@ -1,426 +0,0 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Optional, Union
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
from technical import qtpylib
# This class is a sample. Feel free to customize it.
class SampleStrategy(IStrategy):
"""
This is a sample strategy to inspire you.
More information in https://www.freqtrade.io/en/latest/strategy-customization/
You can:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy
- Add any lib you need to build your strategy
You must keep:
- the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep:
- timeframe, minimal_roi, stoploss, trailing_*
"""
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 3
# Can this strategy go short?
can_short: bool = False
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
minimal_roi = {
# "120": 0.0, # exit after 120 minutes at break even
"60": 0.01,
"30": 0.02,
"0": 0.04,
}
# Optimal stoploss designed for the strategy.
# This attribute will be overridden if the config file contains "stoploss".
stoploss = -0.10
# Trailing stoploss
trailing_stop = False
# trailing_only_offset_is_reached = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Optimal timeframe for the strategy.
timeframe = "5m"
# Run "populate_indicators()" only for new candle.
process_only_new_candles = True
# These values can be overridden in the config.
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True)
short_rsi = IntParameter(low=51, high=100, default=70, space="sell", optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 200
# Optional order type mapping.
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": False,
}
# Optional order time in force.
order_time_in_force = {"entry": "GTC", "exit": "GTC"}
plot_config = {
"main_plot": {
"tema": {},
"sar": {"color": "white"},
},
"subplots": {
"MACD": {
"macd": {"color": "blue"},
"macdsignal": {"color": "orange"},
},
"RSI": {
"rsi": {"color": "red"},
},
},
}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part
of the whitelist as well.
For more information, please consult the documentation
:return: List of tuples in the format (pair, interval)
Sample: return [("ETH/USDT", "5m"),
("BTC/USDT", "15m"),
]
"""
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
# Momentum Indicators
# ------------------------------------
# ADX
dataframe["adx"] = ta.ADX(dataframe)
# # Plus Directional Indicator / Movement
# dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
# dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# # Minus Directional Indicator / Movement
# dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
# dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# # Aroon, Aroon Oscillator
# aroon = ta.AROON(dataframe)
# dataframe['aroonup'] = aroon['aroonup']
# dataframe['aroondown'] = aroon['aroondown']
# dataframe['aroonosc'] = ta.AROONOSC(dataframe)
# # Awesome Oscillator
# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# # Keltner Channel
# keltner = qtpylib.keltner_channel(dataframe)
# dataframe["kc_upperband"] = keltner["upper"]
# dataframe["kc_lowerband"] = keltner["lower"]
# dataframe["kc_middleband"] = keltner["mid"]
# dataframe["kc_percent"] = (
# (dataframe["close"] - dataframe["kc_lowerband"]) /
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"])
# )
# dataframe["kc_width"] = (
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"]
# )
# # Ultimate Oscillator
# dataframe['uo'] = ta.ULTOSC(dataframe)
# # Commodity Channel Index: values [Oversold:-100, Overbought:100]
# dataframe['cci'] = ta.CCI(dataframe)
# RSI
dataframe["rsi"] = ta.RSI(dataframe)
# # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy)
# rsi = 0.1 * (dataframe['rsi'] - 50)
# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
# # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy)
# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# # Stochastic Slow
# stoch = ta.STOCH(dataframe)
# dataframe['slowd'] = stoch['slowd']
# dataframe['slowk'] = stoch['slowk']
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe)
dataframe["fastd"] = stoch_fast["fastd"]
dataframe["fastk"] = stoch_fast["fastk"]
# # Stochastic RSI
# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this.
# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results.
# stoch_rsi = ta.STOCHRSI(dataframe)
# dataframe['fastd_rsi'] = stoch_rsi['fastd']
# dataframe['fastk_rsi'] = stoch_rsi['fastk']
# MACD
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
dataframe["macdhist"] = macd["macdhist"]
# MFI
dataframe["mfi"] = ta.MFI(dataframe)
# # ROC
# dataframe['roc'] = ta.ROC(dataframe)
# Overlap Studies
# ------------------------------------
# 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"]
)
dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe[
"bb_middleband"
]
# Bollinger Bands - Weighted (EMA based instead of SMA)
# weighted_bollinger = qtpylib.weighted_bollinger_bands(
# qtpylib.typical_price(dataframe), window=20, stds=2
# )
# dataframe["wbb_upperband"] = weighted_bollinger["upper"]
# dataframe["wbb_lowerband"] = weighted_bollinger["lower"]
# dataframe["wbb_middleband"] = weighted_bollinger["mid"]
# dataframe["wbb_percent"] = (
# (dataframe["close"] - dataframe["wbb_lowerband"]) /
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"])
# )
# dataframe["wbb_width"] = (
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) /
# dataframe["wbb_middleband"]
# )
# # EMA - Exponential Moving Average
# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
# dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# # SMA - Simple Moving Average
# dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3)
# dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5)
# dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10)
# dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21)
# dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50)
# dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100)
# Parabolic SAR
dataframe["sar"] = ta.SAR(dataframe)
# TEMA - Triple Exponential Moving Average
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe["htsine"] = hilbert["sine"]
dataframe["htleadsine"] = hilbert["leadsine"]
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
# # Hammer: values [0, 100]
# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# # Inverted Hammer: values [0, 100]
# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# # Dragonfly Doji: values [0, 100]
# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# # Piercing Line: values [0, 100]
# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# # Morningstar: values [0, 100]
# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# # Three White Soldiers: values [0, 100]
# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
# # Hanging Man: values [0, 100]
# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# # Shooting Star: values [0, 100]
# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# # Gravestone Doji: values [0, 100]
# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# # Dark Cloud Cover: values [0, 100]
# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# # Evening Doji Star: values [0, 100]
# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# # Evening Star: values [0, 100]
# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
# # Three Line Strike: values [0, -100, 100]
# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# # Spinning Top: values [0, -100, 100]
# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# # Engulfing: values [0, -100, 100]
# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# # Harami: values [0, -100, 100]
# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# # Three Outside Up/Down: values [0, -100, 100]
# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# # Three Inside Up/Down: values [0, -100, 100]
# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
# # Chart type
# # ------------------------------------
# # Heikin Ashi Strategy
# heikinashi = qtpylib.heikinashi(dataframe)
# dataframe['ha_open'] = heikinashi['open']
# dataframe['ha_close'] = heikinashi['close']
# dataframe['ha_high'] = heikinashi['high']
# dataframe['ha_low'] = heikinashi['low']
# Retrieve best bid and best ask from the orderbook
# ------------------------------------
"""
# first check if dataprovider is available
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0]
"""
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with entry columns populated
"""
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe["rsi"], self.buy_rsi.value))
& (dataframe["tema"] <= dataframe["bb_middleband"]) # Guard: tema below BB middle
& (dataframe["tema"] > dataframe["tema"].shift(1)) # Guard: tema is raising
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"enter_long",
] = 1
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe["rsi"], self.short_rsi.value))
& (dataframe["tema"] > dataframe["bb_middleband"]) # Guard: tema above BB middle
& (dataframe["tema"] < dataframe["tema"].shift(1)) # Guard: tema is falling
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit columns populated
"""
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe["rsi"], self.sell_rsi.value))
& (dataframe["tema"] > dataframe["bb_middleband"]) # Guard: tema above BB middle
& (dataframe["tema"] < dataframe["tema"].shift(1)) # Guard: tema is falling
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"exit_long",
] = 1
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe["rsi"], self.exit_short_rsi.value))
&
# Guard: tema below BB middle
(dataframe["tema"] <= dataframe["bb_middleband"])
& (dataframe["tema"] > dataframe["tema"].shift(1)) # Guard: tema is raising
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"exit_short",
] = 1
return dataframe

View File

@ -1,480 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Strategy analysis example\n",
"\n",
"Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n",
"The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location.\n",
"Please follow the [documentation](https://www.freqtrade.io/en/stable/data-download/) for more details."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"### Change Working directory to repository root"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from pathlib import Path\n",
"\n",
"\n",
"# Change directory\n",
"# Modify this cell to insure that the output shows the correct path.\n",
"# Define all paths relative to the project root shown in the cell output\n",
"project_root = \"somedir/freqtrade\"\n",
"i = 0\n",
"try:\n",
" os.chdir(project_root)\n",
" if not Path(\"LICENSE\").is_file():\n",
" i = 0\n",
" while i < 4 and (not Path(\"LICENSE\").is_file()):\n",
" os.chdir(Path(Path.cwd(), \"../\"))\n",
" i += 1\n",
" project_root = Path.cwd()\n",
"except FileNotFoundError:\n",
" print(\"Please define the project root relative to the current directory\")\n",
"print(Path.cwd())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Configure Freqtrade environment"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from freqtrade.configuration import Configuration\n",
"\n",
"\n",
"# Customize these according to your needs.\n",
"\n",
"# Initialize empty configuration object\n",
"config = Configuration.from_files([])\n",
"# Optionally (recommended), use existing configuration file\n",
"# config = Configuration.from_files([\"user_data/config.json\"])\n",
"\n",
"# Define some constants\n",
"config[\"timeframe\"] = \"5m\"\n",
"# Name of the strategy class\n",
"config[\"strategy\"] = \"SampleStrategy\"\n",
"# Location of the data\n",
"data_location = config[\"datadir\"]\n",
"# Pair to analyze - Only use one pair here\n",
"pair = \"BTC/USDT\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Load data using values set above\n",
"from freqtrade.data.history import load_pair_history\n",
"from freqtrade.enums import CandleType\n",
"\n",
"\n",
"candles = load_pair_history(\n",
" datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n",
" pair=pair,\n",
" data_format=\"json\", # Make sure to update this to your data\n",
" candle_type=CandleType.SPOT,\n",
")\n",
"\n",
"# Confirm success\n",
"print(f\"Loaded {len(candles)} rows of data for {pair} from {data_location}\")\n",
"candles.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Load and run strategy\n",
"* Rerun each time the strategy file is changed"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Load strategy using values set above\n",
"from freqtrade.data.dataprovider import DataProvider\n",
"from freqtrade.resolvers import StrategyResolver\n",
"\n",
"\n",
"strategy = StrategyResolver.load_strategy(config)\n",
"strategy.dp = DataProvider(config, None, None)\n",
"strategy.ft_bot_start()\n",
"\n",
"# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {\"pair\": pair})\n",
"df.tail()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Display the trade details\n",
"\n",
"* Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n",
"* Some possible problems\n",
" * Columns with NaN values at the end of the dataframe\n",
" * Columns used in `crossed*()` functions with completely different units\n",
"* Comparison with full backtest\n",
" * having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n",
" * Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available. \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Report results\n",
"print(f\"Generated {df['enter_long'].sum()} entry signals\")\n",
"data = df.set_index(\"date\", drop=False)\n",
"data.tail()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Load existing objects into a Jupyter notebook\n",
"\n",
"The following cells assume that you have already generated data using the cli. \n",
"They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Load backtest results to pandas dataframe\n",
"\n",
"Analyze a trades dataframe (also used below for plotting)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n",
"\n",
"\n",
"# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n",
"backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
"# backtest_dir can also point to a specific file\n",
"# backtest_dir = (\n",
"# config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"\n",
"# )"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# You can get the full backtest statistics by using the following command.\n",
"# This contains all information used to generate the backtest result.\n",
"stats = load_backtest_stats(backtest_dir)\n",
"\n",
"strategy = \"SampleStrategy\"\n",
"# All statistics are available per strategy, so if `--strategy-list` was used during backtest,\n",
"# this will be reflected here as well.\n",
"# Example usages:\n",
"print(stats[\"strategy\"][strategy][\"results_per_pair\"])\n",
"# Get pairlist used for this backtest\n",
"print(stats[\"strategy\"][strategy][\"pairlist\"])\n",
"# Get market change (average change of all pairs from start to end of the backtest period)\n",
"print(stats[\"strategy\"][strategy][\"market_change\"])\n",
"# Maximum drawdown ()\n",
"print(stats[\"strategy\"][strategy][\"max_drawdown_abs\"])\n",
"# Maximum drawdown start and end\n",
"print(stats[\"strategy\"][strategy][\"drawdown_start\"])\n",
"print(stats[\"strategy\"][strategy][\"drawdown_end\"])\n",
"\n",
"\n",
"# Get strategy comparison (only relevant if multiple strategies were compared)\n",
"print(stats[\"strategy_comparison\"])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Load backtested trades as dataframe\n",
"trades = load_backtest_data(backtest_dir)\n",
"\n",
"# Show value-counts per pair\n",
"trades.groupby(\"pair\")[\"exit_reason\"].value_counts()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plotting daily profit / equity line"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n",
"\n",
"import pandas as pd\n",
"import plotly.express as px\n",
"\n",
"from freqtrade.configuration import Configuration\n",
"from freqtrade.data.btanalysis import load_backtest_stats\n",
"\n",
"\n",
"# strategy = 'SampleStrategy'\n",
"# config = Configuration.from_files([\"user_data/config.json\"])\n",
"# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
"\n",
"stats = load_backtest_stats(backtest_dir)\n",
"strategy_stats = stats[\"strategy\"][strategy]\n",
"\n",
"df = pd.DataFrame(columns=[\"dates\", \"equity\"], data=strategy_stats[\"daily_profit\"])\n",
"df[\"equity_daily\"] = df[\"equity\"].cumsum()\n",
"\n",
"fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Load live trading results into a pandas dataframe\n",
"\n",
"In case you did already some trading and want to analyze your performance"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from freqtrade.data.btanalysis import load_trades_from_db\n",
"\n",
"\n",
"# Fetch trades from database\n",
"trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n",
"\n",
"# Display results\n",
"trades.groupby(\"pair\")[\"exit_reason\"].value_counts()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Analyze the loaded trades for trade parallelism\n",
"This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with a very high `max_open_trades` setting.\n",
"\n",
"`analyze_trade_parallelism()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from freqtrade.data.btanalysis import analyze_trade_parallelism\n",
"\n",
"\n",
"# Analyze the above\n",
"parallel_trades = analyze_trade_parallelism(trades, \"5m\")\n",
"\n",
"parallel_trades.plot()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plot results\n",
"\n",
"Freqtrade offers interactive plotting capabilities based on plotly."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from freqtrade.plot.plotting import generate_candlestick_graph\n",
"\n",
"\n",
"# Limit graph period to keep plotly quick and reactive\n",
"\n",
"# Filter trades to one pair\n",
"trades_red = trades.loc[trades[\"pair\"] == pair]\n",
"\n",
"data_red = data[\"2019-06-01\":\"2019-06-10\"]\n",
"# Generate candlestick graph\n",
"graph = generate_candlestick_graph(\n",
" pair=pair,\n",
" data=data_red,\n",
" trades=trades_red,\n",
" indicators1=[\"sma20\", \"ema50\", \"ema55\"],\n",
" indicators2=[\"rsi\", \"macd\", \"macdsignal\", \"macdhist\"],\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Show graph inline\n",
"# graph.show()\n",
"\n",
"# Render graph in a separate window\n",
"graph.show(renderer=\"browser\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plot average profit per trade as distribution graph"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import plotly.figure_factory as ff\n",
"\n",
"\n",
"hist_data = [trades.profit_ratio]\n",
"group_labels = [\"profit_ratio\"] # name of the dataset\n",
"\n",
"fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01)\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data."
]
}
],
"metadata": {
"file_extension": ".py",
"kernelspec": {
"display_name": "Python 3.9.7 64-bit",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
},
"mimetype": "text/x-python",
"name": "python",
"npconvert_exporter": "python",
"pygments_lexer": "ipython3",
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": false
},
"varInspector": {
"cols": {
"lenName": 16,
"lenType": 16,
"lenVar": 40
},
"kernels_config": {
"python": {
"delete_cmd_postfix": "",
"delete_cmd_prefix": "del ",
"library": "var_list.py",
"varRefreshCmd": "print(var_dic_list())"
},
"r": {
"delete_cmd_postfix": ") ",
"delete_cmd_prefix": "rm(",
"library": "var_list.r",
"varRefreshCmd": "cat(var_dic_list()) "
}
},
"types_to_exclude": [
"module",
"function",
"builtin_function_or_method",
"instance",
"_Feature"
],
"window_display": false
},
"version": 3,
"vscode": {
"interpreter": {
"hash": "675f32a300d6d26767470181ad0b11dd4676bcce7ed1dd2ffe2fbc370c95fc7c"
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@ -1,230 +0,0 @@
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from freqtrade.strategy import IStrategy
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class TheForceV7V2(IStrategy):
INTERFACE_VERSION = 2
minimal_roi = {
"0": 10
}
stoploss = -0.1
trailing_stop = False
timeframe = '5m'
process_only_new_candles = False
use_sell_signal = True
sell_profit_only = False
ignore_roi_if_buy_signal = True
startup_candle_count: int = 30
order_types = {
'buy': 'limit',
'sell': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
order_time_in_force = {
'buy': 'gtc',
'sell': 'gtc'
}
plot_config = {
'main_plot': {
'tema': {},
'sar': {'color': 'white'},
},
'subplots': {
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
},
"RSI": {
'rsi': {'color': 'red'},
}
}
}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part
of the whitelist as well.
For more information, please consult the documentation
:return: List of tuples in the format (pair, interval)
Sample: return [("ETH/USDT", "5m"),
("BTC/USDT", "15m"),
]
"""
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
dataframe['rsi7'] = ta.RSI(dataframe, timeperiod=7)
macd = ta.MACD(dataframe,12,26,1)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
dataframe['ema5h'] = ta.EMA(dataframe['high'], timeperiod=5)
dataframe['ema5l'] = ta.EMA(dataframe['low'], timeperiod=5)
dataframe['ema5c'] = ta.EMA(dataframe['close'], timeperiod=5)
dataframe['ema5o'] = ta.EMA(dataframe['open'], timeperiod=5)
dataframe['ema200c'] = ta.MA(dataframe['close'], 200)
dataframe['volvar'] = (dataframe['volume'].rolling(100).mean() * 1.5)
bollinger = qtpylib.bollinger_bands(dataframe['close'], window=21, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper']
dataframe['bb_middleband'] = bollinger['mid']
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
dataframe.loc[
(
(
(
( #Original buy condition
(dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
&
(dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
)
|
( #V3 added based on SmoothScalp
(dataframe['slowk'] < 30) & (dataframe['slowd'] < 30) &
(qtpylib.crossed_above(dataframe['slowk'], dataframe['slowd']))
)
)
&
( #Original buy condition #Might need improvement to have better signals
(dataframe['macd'] > dataframe['macd'].shift(1))
&
(dataframe['macdsignal'] > dataframe['macdsignal'].shift(1))
)
&
( #Original buy condition
(dataframe['close'] > dataframe['close'].shift(1))
& #V6 added condition to improve buy's
(dataframe['open'] > dataframe['open'].shift(1))
)
&
( #Original buy condition
(dataframe['ema5c'] >= dataframe['ema5o'])
|
(dataframe['open'] < dataframe['ema5l'])
)
&
(
(dataframe['volume'] > dataframe['volvar'])
)
)
|
( # V2 Added buy condition w/ Bollingers bands
(dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
&
(dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
&
(
(dataframe['close'] <= dataframe['bb_lowerband'])
|
(dataframe['open'] <= dataframe['bb_lowerband'])
)
)
|
( # V5 added Pullback RSI thanks to simoelmou
(dataframe['close'] > dataframe['ema200c'])
&
(dataframe['rsi7'] < 35)
)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
dataframe.loc[
(
(
(
( #Original sell condition
(dataframe['slowk'] <= 80) & (dataframe['slowd'] <= 80)
)
|
( #V3 added based on SmoothScalp
(qtpylib.crossed_above(dataframe['slowk'], 70))
|
(qtpylib.crossed_above(dataframe['slowd'], 70))
)
)
&
( #Original sell condition
(dataframe['macd'] < dataframe['macd'].shift(1))
&
(dataframe['macdsignal'] < dataframe['macdsignal'].shift(1))
)
&
( #Original sell condition
(dataframe['ema5c'] < dataframe['ema5o'])
|
(dataframe['open'] >= dataframe['ema5h']) # V3 added based on SmoothScalp
)
)
|
( # V2 Added sell condition w/ Bollingers bands
(dataframe['slowk'] <= 80)
&
(dataframe['slowd'] <= 80)
&
(
(dataframe['close'] >= dataframe['bb_upperband'])
|
(dataframe['open'] >= dataframe['bb_upperband'])
)
)
|
(# V6 Added sell condition for extra high values
(dataframe['high'] > dataframe['bb_upperband'])
&
(((dataframe['high'] - dataframe['bb_upperband']) * 100 / dataframe['bb_upperband']) > 1)
)
),
'sell'] = 1
return dataframe

View File

@ -1,206 +0,0 @@
1,4d0
< import numpy as np # noqa
< import pandas as pd # noqa
< from pandas import DataFrame
<
6c2,3
<
---
> import pandas as pd
> import numpy as np
12,13c9
<
< INTERFACE_VERSION = 2
---
> INTERFACE_VERSION = 3
14a11
> # 基础参数
16c13
< "0": 10
---
> "0": 10
36d32
< plot_config = {
37a34,35
> # 绘图配置
> plot_config = {
51a50
>
65c64
< def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
---
> def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
77,78d75
<
<
85c82
< macd = ta.MACD(dataframe,12,26,1)
---
> macd = ta.MACD(dataframe, 12, 26, 1)
102,103c99
<
<
---
>
106c102
< def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
---
> def populate_buy_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
116,130c112,113
< (
< ( #Original buy condition
< (dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
< &
< (dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
< )
< |
< ( #V3 added based on SmoothScalp
< (dataframe['slowk'] < 30) & (dataframe['slowd'] < 30) &
< (qtpylib.crossed_above(dataframe['slowk'], dataframe['slowd']))
< )
< )
< &
< ( #Original buy condition #Might need improvement to have better signals
< (dataframe['macd'] > dataframe['macd'].shift(1))
---
> (
> (dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
132c115
< (dataframe['macdsignal'] > dataframe['macdsignal'].shift(1))
---
> (dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
134,149c117,120
< &
< ( #Original buy condition
< (dataframe['close'] > dataframe['close'].shift(1))
< & #V6 added condition to improve buy's
< (dataframe['open'] > dataframe['open'].shift(1))
< )
< &
< ( #Original buy condition
< (dataframe['ema5c'] >= dataframe['ema5o'])
< |
< (dataframe['open'] < dataframe['ema5l'])
< )
< &
< (
<
< (dataframe['volume'] > dataframe['volvar'])
---
> |
> (
> (dataframe['slowk'] < 30) & (dataframe['slowd'] < 30) &
> (qtpylib.crossed_above(dataframe['slowk'], dataframe['slowd']))
152,154c123,125
< |
< ( # V2 Added buy condition w/ Bollingers bands
< (dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
---
> &
> (
> (dataframe['macd'] > dataframe['macd'].shift(1))
156c127,131
< (dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
---
> (dataframe['macdsignal'] > dataframe['macdsignal'].shift(1))
> )
> &
> (
> (dataframe['close'] > dataframe['close'].shift(1))
158,162c133
< (
< (dataframe['close'] <= dataframe['bb_lowerband'])
< |
< (dataframe['open'] <= dataframe['bb_lowerband'])
< )
---
> (dataframe['open'] > dataframe['open'].shift(1))
164,168c135,143
< |
< ( # V5 added Pullback RSI thanks to simoelmou
< (dataframe['close'] > dataframe['ema200c'])
< &
< (dataframe['rsi7'] < 35)
---
> &
> (
> (dataframe['ema5c'] >= dataframe['ema5o'])
> |
> (dataframe['open'] < dataframe['ema5l'])
> )
> &
> (
> (dataframe['volume'] > dataframe['volvar'])
175c150
< def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
---
> def populate_sell_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
185,200c160,161
< (
< ( #Original sell condition
< (dataframe['slowk'] <= 80) & (dataframe['slowd'] <= 80)
< )
< |
< ( #V3 added based on SmoothScalp
< (qtpylib.crossed_above(dataframe['slowk'], 70))
< |
< (qtpylib.crossed_above(dataframe['slowd'], 70))
< )
< )
< &
< ( #Original sell condition
< (dataframe['macd'] < dataframe['macd'].shift(1))
< &
< (dataframe['macdsignal'] < dataframe['macdsignal'].shift(1))
---
> (
> (dataframe['slowk'] <= 80) & (dataframe['slowd'] <= 80)
202,204c163,165
< &
< ( #Original sell condition
< (dataframe['ema5c'] < dataframe['ema5o'])
---
> |
> (
> (qtpylib.crossed_above(dataframe['slowk'], 70))
206c167
< (dataframe['open'] >= dataframe['ema5h']) # V3 added based on SmoothScalp
---
> (qtpylib.crossed_above(dataframe['slowd'], 70))
209,213c170,172
< |
< ( # V2 Added sell condition w/ Bollingers bands
< (dataframe['slowk'] <= 80)
< &
< (dataframe['slowd'] <= 80)
---
> &
> (
> (dataframe['macd'] < dataframe['macd'].shift(1))
215,219c174
< (
< (dataframe['close'] >= dataframe['bb_upperband'])
< |
< (dataframe['open'] >= dataframe['bb_upperband'])
< )
---
> (dataframe['macdsignal'] < dataframe['macdsignal'].shift(1))
221,225c176,180
< |
< (# V6 Added sell condition for extra high values
< (dataframe['high'] > dataframe['bb_upperband'])
< &
< (((dataframe['high'] - dataframe['bb_upperband']) * 100 / dataframe['bb_upperband']) > 1)
---
> &
> (
> (dataframe['ema5c'] < dataframe['ema5o'])
> |
> (dataframe['open'] >= dataframe['ema5h'])
227d181
<
230,231c184
< return dataframe
<
---
>

74
tools/analytic.py Normal file
View File

@ -0,0 +1,74 @@
import pandas as pd
# 加载交易记录
df = pd.read_csv('../result/backtest_trades.csv')
# 转换日期格式
df['open_date'] = pd.to_datetime(df['open_date'])
df['close_date'] = pd.to_datetime(df['close_date'])
# 计算持仓天数
df['holding_days'] = (df['close_date'] - df['open_date']).dt.total_seconds() / (60 * 60 * 24)
# 按币种分组
grouped_by_pair = df.groupby('pair').agg(
total_trades=('profit_abs', 'size'),
avg_profit_ratio=('profit_ratio', 'mean'),
total_profit_abs=('profit_abs', 'sum'),
win_rate=('profit_ratio', lambda x: (x > 0).mean()),
avg_duration=('trade_duration', 'mean')
)
print(grouped_by_pair)
#按退出原因分组
grouped_by_exit = df.groupby('exit_reason').agg(
total_trades=('profit_abs', 'size'),
avg_profit_ratio=('profit_ratio', 'mean'),
total_profit_abs=('profit_abs', 'sum'),
win_rate=('profit_ratio', lambda x: (x > 0).mean())
)
print(grouped_by_exit)
#按月份分组统计
df['open_date_naive'] = df['open_date'].dt.tz_localize(None)
df['month'] = df['open_date_naive'].dt.to_period('M')
grouped_by_month = df.groupby('month').agg(
total_trades=('profit_abs', 'size'),
avg_profit_ratio=('profit_ratio', 'mean'),
total_profit_abs=('profit_abs', 'sum'),
win_rate=('profit_ratio', lambda x: (x > 0).mean())
)
print(grouped_by_month)
# 按盈利区间分组统计
bins = [-float('inf'), -0.05, -0.02, 0, 0.02, 0.05, float('inf')]
labels = ['<-5%', '-5%~-2%', '-2%~0%', '0%~2%', '2%~5%', '>5%']
df['profit_group'] = pd.cut(df['profit_ratio'], bins=bins, labels=labels)
grouped_by_profit = df.groupby('profit_group', observed=True).agg(
count=('profit_abs', 'size'),
avg_profit=('profit_ratio', 'mean'),
total_profit=('profit_abs', 'sum')
)
print(grouped_by_profit)
# 分组为短中长线
df['duration_group'] = pd.cut(df['trade_duration'],
bins=[0, 60, 360, 1440, 100000],
labels=['<1小时', '1~6小时', '6小时~1天', '>1天'])
grouped_by_duration = df.groupby('duration_group', observed=True).agg(
count=('profit_abs', 'size'),
avg_profit=('profit_ratio', 'mean'),
win_rate=('profit_ratio', lambda x: (x > 0).mean()),
total_profit=('profit_abs', 'sum')
)
print(grouped_by_duration)

4
tools/analytic.sh Executable file
View File

@ -0,0 +1,4 @@
cd ../
source .venv/bin/activate
cd -
python analytic.py

View File

@ -31,15 +31,18 @@ rm -rf user_data/models/*
rm -rf ./freqtrade/user_data/data/backtest_results/*
rm -fr ./user_data/dryrun_results/*
hyperopt_config="${STRATEGY_NAME%.py}.json"
#docker-compose -f docker-compose_backtest.yml run --rm freqtrade >output.log 2>&1
freqtrade backtesting \
--logfile ./user_data/logs/freqtrade.log \
--freqaimodel XGBoostRegressor \
--strategy $STRATEGY_NAME \
--config ./freqtrade/templates/$hyperopt_config \
--config config_examples/$CONFIG_FILE \
--strategy-path ./freqtrade/templates \
--timerange ${START_DATE}-${END_DATE} \
--breakdown week month \
--enable-protections \
--export trades \
--fee 0.0016 \
--cache none >output.log 2>&1
@ -71,4 +74,5 @@ sed -i 's/\x1B\[[0-9;]*m//g' output.log
cp output.log result/ -f
cd tools/
python tradestocsv.py
python analytic.py >../result/analytic.log
cd ../

76
tools/hyperopt.sh Executable file
View File

@ -0,0 +1,76 @@
#!/bin/bash
# 检查 .env 文件
if [ ! -f ".env" ]; then
echo "⚠️ 本地缺少 .env 文件,请创建并配置。示例内容如下:"
echo ""
echo "STRATEGY_NAME=TheForceV7"
echo "CONFIG_FILE=basic.json"
echo "TEST_BRANCH=theforce-noai-test"
echo "DRYRUN_BRANCH=theforce-noai-dryrun"
echo ""
exit 1
fi
# 加载 .env 文件中的变量
export $(grep -v '^#' .env | xargs)
# 设置默认值
STRATEGY_NAME=${STRATEGY_NAME:-TheForceV7}
CONFIG_FILE=${CONFIG_FILE:-basic.json}
echo "Using strategy: $STRATEGY_NAME"
echo "Using config: $CONFIG_FILE"
echo "Using testBranch: $TEST_BRANCH"
# Parse command line arguments
START_DATE=${1:-$(date -d "2 days ago" +"%Y%m%d")}
END_DATE=${2:-$(date -d "tomorrow" +"%Y%m%d")}
cd ../
source .venv/bin/activate
rm -rf user_data/models/*
rm -rf ./freqtrade/user_data/data/backtest_results/*
rm -fr ./user_data/dryrun_results/*
#docker-compose -f docker-compose_backtest.yml run --rm freqtrade >output.log 2>&1
freqtrade hyperopt \
--logfile ./user_data/logs/freqtrade.log \
--freqaimodel XGBoostRegressor \
--strategy $STRATEGY_NAME \
--config config_examples/$CONFIG_FILE \
--strategy-path ./freqtrade/templates \
--timerange ${START_DATE}-${END_DATE} \
--epochs 100 \
--hyperopt-loss ShortTradeDurHyperOptLoss \
--spaces stoploss \
--fee 0.0016
#>output.log 2>&1
#sed -i 's/\x1B\[[0-9;]*m//g' output.log
#python3 tools/filter.py
rm ./result/*.json -fr
rm ./result/*.py -fr
mv ./user_data/backtest_results/* ./result/
cd ./result
# 查找当前目录下的所有 zip 文件
zip_files=(*.zip)
# 检查是否只有一个 zip 文件
if [ ${#zip_files[@]} -eq 1 ]; then
# 解压缩该 zip 文件到当前目录
unzip "${zip_files[0]}"
rm *.zip
rm *.feather
else
echo "当前目录下没有 zip 文件或者有多个 zip 文件,无法操作。"
fi
cd -
sed -i 's/\x1B\[[0-9;]*m//g' output.log
#python3 ../filter.py
cp output.log result/ -f
cd tools/
python tradestocsv.py
python analytic.py >../result/analytic.log
cd ../