update ignore
This commit is contained in:
parent
7bf5919368
commit
5103976971
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,7 +4,7 @@
|
||||
*.sqlite-wal
|
||||
logfile.txt
|
||||
user_data/*
|
||||
!user_data/strategy/sample_strategy.py
|
||||
!user_data/strategies
|
||||
!user_data/notebooks
|
||||
!user_data/models
|
||||
!user_data/freqaimodels
|
||||
|
||||
44
user_data/strategies/aa.json
Normal file
44
user_data/strategies/aa.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"strategy_name": "FreqaiPrimer",
|
||||
"params": {
|
||||
"stoploss": {
|
||||
"stoploss": -0.05
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
},
|
||||
"buy": {
|
||||
"ADD_POSITION_THRESHOLD": -0.046,
|
||||
"BUY_THRESHOLD_MAX": -0.01,
|
||||
"BUY_THRESHOLD_MIN": -0.026,
|
||||
"COOLDOWN_PERIOD_MINUTES": 9,
|
||||
"MAX_ENTRY_POSITION_ADJUSTMENT": 3
|
||||
},
|
||||
"sell": {
|
||||
"EXIT_POSITION_RATIO": 0.572,
|
||||
"HOLD_TIME_EXTENSION": 1.975,
|
||||
"HOLD_TIME_REDUCTION": 0.832,
|
||||
"MAX_HOLD_TIME_MINUTES": 101,
|
||||
"SELL_THRESHOLD_MAX": 0.085,
|
||||
"SELL_THRESHOLD_MIN": 0.007,
|
||||
"SLOPE_THRESHOLD": 0.0,
|
||||
"TRAILING_STOP_DISTANCE": 0.005,
|
||||
"TRAILING_STOP_START": 0.048
|
||||
},
|
||||
"protection": {},
|
||||
"roi": {
|
||||
"0": 0.128,
|
||||
"12": 0.038,
|
||||
"36": 0.023,
|
||||
"53": 0
|
||||
},
|
||||
"trailing": {
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.09,
|
||||
"trailing_stop_positive_offset": 0.16499999999999998,
|
||||
"trailing_only_offset_is_reached": false
|
||||
}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-06-27 03:30:20.329593+00:00"
|
||||
}
|
||||
67
user_data/strategies/aaa.diff
Normal file
67
user_data/strategies/aaa.diff
Normal file
@ -0,0 +1,67 @@
|
||||
--- original_strategy.py
|
||||
+++ modified_strategy.py
|
||||
@@ -28,8 +28,8 @@ class FreqaiPrimer(IStrategy):
|
||||
TRAILING_STOP_DISTANCE = DecimalParameter(0.005, 0.02, default=0.01, space='sell', optimize=True)
|
||||
|
||||
- BUY_THRESHOLD_MIN = DecimalParameter(-0.1, -0.01, default=-0.05, space='buy', optimize=True)
|
||||
- BUY_THRESHOLD_MAX = DecimalParameter(-0.02, -0.001, default=-0.005, space='buy', optimize=True)
|
||||
+ BUY_THRESHOLD_MIN = DecimalParameter(-0.15, -0.03, default=-0.07, space='buy', optimize=True)
|
||||
+ BUY_THRESHOLD_MAX = DecimalParameter(-0.04, -0.005, default=-0.01, space='buy', optimize=True)
|
||||
SELL_THRESHOLD_MIN = DecimalParameter(0.001, 0.02, default=0.005, space='sell', optimize=True)
|
||||
SELL_THRESHOLD_MAX = DecimalParameter(0.02, 0.1, default=0.05, space='sell', optimize=True)
|
||||
|
||||
@@ -356,10 +356,10 @@ class FreqaiPrimer(IStrategy):
|
||||
volume_z_score_min = 0.5
|
||||
volume_z_score_max = 1.5
|
||||
volume_z_score_threshold = self.linear_map(trend_score, 0, 100, volume_z_score_max, volume_z_score_min)
|
||||
|
||||
- rsi_min = 40
|
||||
- rsi_max = 60
|
||||
+ rsi_min = 35
|
||||
+ rsi_max = 55
|
||||
rsi_threshold = self.linear_map(trend_score, 0, 100, rsi_max, rsi_min)
|
||||
|
||||
- stochrsi_min = 30
|
||||
- stochrsi_max = 50
|
||||
+ stochrsi_min = 25
|
||||
+ stochrsi_max = 45
|
||||
stochrsi_threshold = self.linear_map(trend_score, 0, 100, stochrsi_max, stochrsi_min)
|
||||
|
||||
if "&-price_value_divergence" in dataframe.columns:
|
||||
# 计算 STOCHRSI
|
||||
stochrsi = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
|
||||
dataframe["stochrsi_k"] = stochrsi["fastk"]
|
||||
+ # 添加 15m 时间框架的 EMA200
|
||||
+ dataframe_15m = self.dp.get_pair_dataframe(pair=pair, timeframe="15m")
|
||||
+ dataframe_15m["ema200_15m"] = ta.EMA(dataframe_15m, timeperiod=200)
|
||||
+ dataframe["ema200_15m"] = dataframe_15m["ema200_15m"].reindex(dataframe.index, method="ffill")
|
||||
|
||||
cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold)
|
||||
cond2 = (dataframe["volume_z_score"] > volume_z_score_threshold)
|
||||
cond3 = (dataframe["rsi"] < rsi_threshold)
|
||||
cond4 = (dataframe["close"] <= dataframe["bb_lowerband"])
|
||||
- cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold)
|
||||
- buy_condition = cond1 & cond2 & cond3 & cond4 & cond5
|
||||
+ cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold)
|
||||
+ cond6 = (dataframe["close"] < dataframe["ema200_15m"]) # 价格低于 15m EMA200
|
||||
+ buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6
|
||||
conditions.append(buy_condition)
|
||||
|
||||
@@ -534,7 +534,7 @@ class FreqaiPrimer(IStrategy):
|
||||
def get_market_trend(self, dataframe: DataFrame = None, metadata: dict = None) -> int:
|
||||
try:
|
||||
timeframes = ["3m", "15m", "1h"]
|
||||
- weights = {"3m": 0.5, "15m": 0.3, "1h": 0.2}
|
||||
+ weights = {"3m": 0.3, "15m": 0.35, "1h": 0.35}
|
||||
trend_scores = {}
|
||||
pair = metadata.get('pair', 'Unknown') if metadata else 'Unknown'
|
||||
|
||||
@@ -565,7 +565,7 @@ class FreqaiPrimer(IStrategy):
|
||||
pair_df["ema_short_slope"] = (pair_df["ema_short"] - pair_df["ema_short"].shift(10)) / pair_df["ema_short"].shift(10)
|
||||
|
||||
# 动态调整权重
|
||||
- if trend_scores.get("1h", 50) - trend_scores.get("3m", 50) > 20:
|
||||
- weights = {"3m": 0.3, "15m": 0.3, "1h": 0.4}
|
||||
+ if trend_scores.get("1h", 50) - trend_scores.get("3m", 50) > 20 or trend_scores.get("15m", 50) - trend_scores.get("3m", 50) > 20:
|
||||
+ weights = {"3m": 0.2, "15m": 0.35, "1h": 0.45}
|
||||
logger.debug(f"[{pair}] 1h 趋势得分({trend_scores.get('1h', 50)})显著高于 3m({trend_scores.get('3m', 50)}),调整权重为 {weights}")
|
||||
85
user_data/strategies/chat.md
Normal file
85
user_data/strategies/chat.md
Normal file
@ -0,0 +1,85 @@
|
||||
> 请把当前策略的运行逻辑进行梳理和讲解
|
||||
|
||||
我来为您梳理和讲解这个策略的运行逻辑。这是一个基于 FreqAI 的机器学习交易策略,主要使用 RSI 指标和机器学习模型进行交易决策。以下是策略的主要逻辑:
|
||||
|
||||
### 1. 策略配置
|
||||
```python
|
||||
minimal_roi = {
|
||||
"0": 0.02,
|
||||
"7": 0.01,
|
||||
"13": 0.005,
|
||||
"60": 0
|
||||
}
|
||||
stoploss = 0.0
|
||||
trailing_stop = True
|
||||
```
|
||||
- 设置了静态的 ROI 目标
|
||||
- 启用了追踪止损
|
||||
|
||||
### 2. 特征工程
|
||||
策略通过多个方法构建特征:
|
||||
- `feature_engineering_expand_all`: 计算 RSI 和布林带宽度
|
||||
- `feature_engineering_expand_basic`: 计算价格变化百分比
|
||||
- `feature_engineering_standard`: 计算波动率和时间特征
|
||||
|
||||
### 3. 目标变量设置
|
||||
在 `set_freqai_targets` 方法中:
|
||||
- 使用未来 `label_period_candles` 周期的 RSI 值作为目标变量
|
||||
- 计算并标准化波动率特征
|
||||
|
||||
### 4. 指标计算
|
||||
`populate_indicators` 方法中计算了多个技术指标:
|
||||
- RSI
|
||||
- 波动率
|
||||
- TEMA(三重指数移动平均线)
|
||||
- 布林带下轨
|
||||
- 未来价格方向(up_or_down)
|
||||
|
||||
### 5. 机器学习模型
|
||||
使用 LightGBM 回归模型:
|
||||
```python
|
||||
freqai_info = {
|
||||
"model": "LightGBMRegressor",
|
||||
"feature_parameters": {...},
|
||||
"data_split_parameters": {...},
|
||||
"model_training_parameters": {...}
|
||||
}
|
||||
```
|
||||
- 模型预测买入和卖出的 RSI 值
|
||||
- 使用 SOL/USDT 作为相关交易对
|
||||
|
||||
### 6. 入场逻辑
|
||||
`populate_entry_trend` 方法中:
|
||||
- 当实际 RSI 上穿预测的买入 RSI 时
|
||||
- TEMA 指标向上
|
||||
- 交易量大于 0
|
||||
- 模型预测结果为 1
|
||||
- 未来价格方向为上涨
|
||||
|
||||
### 7. 出场逻辑
|
||||
`populate_exit_trend` 方法中:
|
||||
- 当实际 RSI 上穿预测的卖出 RSI 时
|
||||
- 或价格下跌超过 2%
|
||||
- 或价格跌破布林带下轨
|
||||
- 或持有时间超过 1 天
|
||||
|
||||
### 8. 交易确认
|
||||
`confirm_trade_entry` 方法:
|
||||
- 检查当前价格是否在可接受范围内
|
||||
- 记录交易确认信息
|
||||
|
||||
### 策略特点
|
||||
1. 机器学习驱动:使用 LightGBM 模型预测 RSI 值
|
||||
2. 多特征融合:结合技术指标、波动率和时间特征
|
||||
3. 风险控制:设置了 ROI 目标和追踪止损
|
||||
4. 动态调整:通过机器学习模型动态调整交易信号
|
||||
|
||||
### 运行流程
|
||||
1. 数据准备:获取历史数据,计算技术指标
|
||||
2. 特征工程:构建机器学习特征
|
||||
3. 模型训练:使用历史数据训练 LightGBM 模型
|
||||
4. 信号生成:根据模型预测结果生成交易信号
|
||||
5. 交易执行:根据入场和出场逻辑执行交易
|
||||
6. 风险控制:根据 ROI 和止损规则管理仓位
|
||||
|
||||
这个策略结合了传统技术分析和机器学习方法,通过模型预测来增强交易信号的准确性,同时保持了严格的风险控制机制。
|
||||
64
user_data/strategies/df.diff
Normal file
64
user_data/strategies/df.diff
Normal file
@ -0,0 +1,64 @@
|
||||
--- original_strategy.py
|
||||
+++ modified_strategy.py
|
||||
@@ -297,6 +297,9 @@ class FreqaiPrimer(IStrategy):
|
||||
dataframe["bb_lowerband"] = lowerband
|
||||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
dataframe["volume_mean_20"] = dataframe["volume"].rolling(20).mean()
|
||||
dataframe["volume_std_20"] = dataframe["volume"].rolling(20).std()
|
||||
dataframe["volume_z_score"] = (dataframe["volume"] - dataframe["volume_mean_20"]) / dataframe["volume_std_20"]
|
||||
+ # 计算15分钟ATR和3分钟EMA10、EMA20
|
||||
+ dataframe_15m = self.dp.get_pair_dataframe(pair=pair, timeframe="15m")
|
||||
+ cuyuan
|
||||
+ dataframe_15m['atr15m'] = ta.ATR(dataframe_15m, timeperiod=14)
|
||||
+ dataframe['atr15m'] = dataframe_15m['atr15m'].reindex(dataframe.index, method="ffill")
|
||||
+ dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
+ dataframe['ema20'] = ta.EMA(dataframe, timeperiod=20)
|
||||
|
||||
@@ -390,8 +390,11 @@ class FreqaiPrimer(IStrategy):
|
||||
conditions = []
|
||||
|
||||
if "&-price_value_divergence" in dataframe.columns:
|
||||
cond1 = (dataframe["&-price_value_divergence"] > self.sell_threshold * 1.025)
|
||||
- cond2 = (dataframe["rsi"] > 75)
|
||||
+ rsi_threshold = 70 if trend_score > 50 else 65 # 动态RSI阈值
|
||||
+ cond2 = (dataframe["rsi"] > rsi_threshold)
|
||||
+ cond3 = (dataframe['ema10'] < dataframe['ema20']) # 短期均线交叉
|
||||
sell_condition = cond1 | cond2
|
||||
conditions.append(sell_condition)
|
||||
|
||||
@@ -442,6 +442,13 @@ class FreqaiPrimer(IStrategy):
|
||||
initial_stake_amount = trade.stake_amount / 3
|
||||
logger.debug(f"{pair} 首次入场金额: {initial_stake_amount:.2f}, 当前持仓金额: {trade.stake_amount:.2f}, "
|
||||
f"加仓次数: {trade.nr_of_successful_entries - 1}")
|
||||
|
||||
+ # 分批出场:短期涨幅触发
|
||||
+ short_term_return = (current_rate - dataframe['close'].shift(3).iloc[-1]) / dataframe['close'].shift(3).iloc[-1]
|
||||
+ if short_term_return >= 0.03: # 3%涨幅
|
||||
+ reduce_amount = -0.5 * trade.stake_amount # 卖出50%仓位
|
||||
+ logger.info(f"{pair} 短期涨幅 {short_term_return*100:.2f}%,卖出50%仓位 {abs(reduce_amount):.2f}")
|
||||
+ return (reduce_amount, f"Short-term gain {short_term_return*100:.2f}%")
|
||||
+
|
||||
# 加仓逻辑
|
||||
max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT.value
|
||||
@@ -484,12 +484,11 @@ class FreqaiPrimer(IStrategy):
|
||||
# 追踪止损逻辑
|
||||
trailing_stop_start = self.TRAILING_STOP_START.value
|
||||
trailing_stop_distance = self.TRAILING_STOP_DISTANCE.value
|
||||
- # 使用 Sigmoid 映射调整追踪止损参数
|
||||
- sigmoid = 1 / (1 + np.exp(-0.1 * (trend_score - 50)))
|
||||
- trailing_factor = 0.8 + (1.2 - 0.8) * sigmoid # 牛市(100) -> 1.2, 熊市(0) -> 0.8
|
||||
- distance_factor = 0.7 + (1.5 - 0.7) * sigmoid # 牛市(100) -> 1.5, 熊市(0) -> 0.7
|
||||
- trailing_stop_start *= trailing_factor
|
||||
- trailing_stop_distance *= distance_factor
|
||||
+ # 使用15分钟ATR动态调整止损距离
|
||||
+ atr15m = dataframe['atr15m'].iloc[-1]
|
||||
+ trailing_stop_distance = min(max(2 * atr15m / current_rate, 0.01), 0.05) # 2倍ATR,限制在0.01-0.05
|
||||
+ if trend_score > 50: # 强趋势时放宽止损
|
||||
+ trailing_stop_distance *= 1.2
|
||||
+ trailing_stop_price = max_rate * (1 - trailing_stop_distance)
|
||||
|
||||
if profit_ratio >= trailing_stop_start and not self.trailing_stop_enabled:
|
||||
self.trailing_stop_enabled = True
|
||||
trade.adjust_min_max_rates(current_rate, current_rate)
|
||||
logger.info(f"{pair} 价格上涨超过 {trailing_stop_start*100:.1f}%,启动追踪止损")
|
||||
return None
|
||||
222
user_data/strategies/freqai_primer_ml.py
Normal file
222
user_data/strategies/freqai_primer_ml.py
Normal file
@ -0,0 +1,222 @@
|
||||
import logging
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
|
||||
import pandas_ta as ta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FreqaiPrimerML(IStrategy):
|
||||
"""
|
||||
基于 FreqAI ML 的交易策略 - 单目标回归模型
|
||||
预测未来价格上升幅度,用于入场决策
|
||||
"""
|
||||
|
||||
# FreqAI 配置 - 直接在类中定义
|
||||
freqai_info = {
|
||||
"identifier": "freqai_primer_ml",
|
||||
"model": "LightGBMRegressor",
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["3m", "15m", "1h"],
|
||||
"include_shifted_candles": 3,
|
||||
"label_period_candles": 6,
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.2,
|
||||
"shuffle": False,
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 200,
|
||||
"learning_rate": 0.05,
|
||||
"num_leaves": 31,
|
||||
"verbose": -1,
|
||||
},
|
||||
"fit_live_predictions_candles": 100,
|
||||
"live_retrain_candles": 100,
|
||||
}
|
||||
|
||||
# 策略参数
|
||||
stoploss = -0.15
|
||||
trailing_stop = True
|
||||
trailing_stop_positive_offset = 0.005
|
||||
timeframe = "3m"
|
||||
can_short = False
|
||||
|
||||
# 可优化参数
|
||||
bb_std = DecimalParameter(2.0, 5.0, decimals=1, default=3.5, optimize=True, space='buy')
|
||||
rsi_length = IntParameter(10, 30, default=14, optimize=True, space='buy')
|
||||
rsi_oversold = IntParameter(20, 50, default=30, optimize=True, space='buy')
|
||||
|
||||
def informative_pairs(self):
|
||||
pairs = self.dp.current_whitelist()
|
||||
return [(pair, '15m') for pair in pairs] + [(pair, '1h') for pair in pairs]
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame:
|
||||
"""
|
||||
为 FreqAI 生成扩展特征
|
||||
"""
|
||||
# RSI 特征
|
||||
dataframe[f"%-rsi-{period}"] = ta.rsi(dataframe["close"], length=period)
|
||||
|
||||
# 成交量特征
|
||||
dataframe[f"%-volume_ratio-{period}"] = dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||||
|
||||
# ADX 趋势强度
|
||||
adx_result = ta.adx(dataframe["high"], dataframe["low"], dataframe["close"], length=period)
|
||||
dataframe[f"%-adx-{period}"] = adx_result[f"ADX_{period}"]
|
||||
|
||||
# 价格位置特征
|
||||
low = dataframe["low"].rolling(period).min()
|
||||
high = dataframe["high"].rolling(period).max()
|
||||
dataframe[f"%-price_position-{period}"] = (dataframe["close"] - low) / (high - low + 1e-8)
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||
"""
|
||||
标准特征(不需要自动扩展)
|
||||
"""
|
||||
# 小时时间特征
|
||||
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
||||
|
||||
# ATR 波动性
|
||||
atr_14 = ta.atr(dataframe["high"], dataframe["low"], dataframe["close"], length=14)
|
||||
dataframe["%-atr_14"] = atr_14 / dataframe["close"]
|
||||
|
||||
# EMA 比率
|
||||
ema_5 = ta.ema(dataframe["close"], length=5)
|
||||
ema_10 = ta.ema(dataframe["close"], length=10)
|
||||
dataframe["%-ema_ratio"] = ema_5 / (ema_10 + 1e-8)
|
||||
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||
"""
|
||||
为 FreqAI 生成回归目标:预测未来价格上升幅度
|
||||
"""
|
||||
pair = metadata.get('pair', 'Unknown')
|
||||
|
||||
# 获取标签周期
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
|
||||
# 计算未来 label_period 根 K 线内的最大上升幅度
|
||||
dataframe["&-price_rise"] = 0.0
|
||||
|
||||
for i in range(len(dataframe) - label_period):
|
||||
future_high = dataframe["high"].iloc[i:i+label_period].max()
|
||||
current_price = dataframe["close"].iloc[i]
|
||||
|
||||
# 计算上升幅度(百分比)
|
||||
max_rise_pct = (future_high - current_price) / current_price if current_price != 0 else 0
|
||||
dataframe.loc[i, "&-price_rise"] = max(0, max_rise_pct) # 只记录上升,下跌记为0
|
||||
|
||||
# 平滑处理
|
||||
dataframe["&-price_rise"] = dataframe["&-price_rise"].rolling(3).mean().fillna(0)
|
||||
|
||||
logger.debug(f"[{pair}] 目标标签统计: 均值={dataframe['&-price_rise'].mean():.6f}, "
|
||||
f"最小={dataframe['&-price_rise'].min():.6f}, "
|
||||
f"最大={dataframe['&-price_rise'].max():.6f}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
主指标计算函数 - FreqAI 在最开始调用
|
||||
"""
|
||||
pair = metadata.get('pair', 'Unknown')
|
||||
|
||||
# 🔧 第一步:调用 FreqAI 进行预测和训练
|
||||
if hasattr(self, 'freqai') and self.freqai is not None:
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
logger.debug(f"[{pair}] FreqAI 预测已生成,列: {list(dataframe.columns)}")
|
||||
else:
|
||||
logger.warning(f"[{pair}] FreqAI 未初始化")
|
||||
|
||||
# 计算基础指标
|
||||
rsi_length = self.rsi_length.value
|
||||
bb_std = self.bb_std.value
|
||||
bb_length = 40
|
||||
|
||||
# 布林带
|
||||
bb = ta.bbands(dataframe['close'], length=bb_length, std=bb_std)
|
||||
dataframe['bb_lower'] = bb[f'BBL_{bb_length}_{bb_std}']
|
||||
dataframe['bb_upper'] = bb[f'BBU_{bb_length}_{bb_std}']
|
||||
dataframe['bb_middle'] = bb[f'BBM_{bb_length}_{bb_std}']
|
||||
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.rsi(dataframe['close'], length=rsi_length)
|
||||
|
||||
# EMA
|
||||
dataframe['ema_50'] = ta.ema(dataframe['close'], length=50)
|
||||
dataframe['ema_200'] = ta.ema(dataframe['close'], length=200)
|
||||
|
||||
# 成交量均线
|
||||
dataframe['volume_ma'] = dataframe['volume'].rolling(20).mean()
|
||||
|
||||
# MACD
|
||||
macd = ta.macd(dataframe['close'])
|
||||
dataframe['macd'] = macd['MACD_12_26_9']
|
||||
dataframe['macd_signal'] = macd['MACDs_12_26_9']
|
||||
|
||||
# 填充缺失值
|
||||
dataframe = dataframe.fillna(0).bfill()
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
入场逻辑 - 使用 ML 预测
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# 条件1: 价格接近下轨
|
||||
conditions.append(dataframe['close'] <= dataframe['bb_lower'] * 1.02)
|
||||
|
||||
# 条件2: RSI 超卖
|
||||
conditions.append(dataframe['rsi'] < self.rsi_oversold.value)
|
||||
|
||||
# 条件3: MACD 底部
|
||||
conditions.append(dataframe['macd'] < dataframe['macd_signal'])
|
||||
|
||||
# 条件4: EMA 多头排列
|
||||
conditions.append(dataframe['ema_50'] > dataframe['ema_200'])
|
||||
|
||||
# 传统条件
|
||||
traditional_signal = (conditions[0] & conditions[1] & conditions[2] & conditions[3])
|
||||
|
||||
# ML 增强:使用 FreqAI 预测的价格上升幅度
|
||||
if '&-price_rise' in dataframe.columns:
|
||||
ml_signal = dataframe['&-price_rise'] > 0.01 # 预测上升 > 1%
|
||||
final_signal = traditional_signal & ml_signal
|
||||
logger.debug(f"[{metadata['pair']}] 使用 ML 增强的入场信号")
|
||||
else:
|
||||
final_signal = traditional_signal
|
||||
logger.debug(f"[{metadata['pair']}] 仅使用传统入场信号(ML 未初始化)")
|
||||
|
||||
dataframe.loc[final_signal, 'enter_long'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
出场逻辑
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# 条件1: 价格突破上轨
|
||||
conditions.append(dataframe['close'] >= dataframe['bb_upper'] * 0.99)
|
||||
|
||||
# 条件2: RSI 超买
|
||||
conditions.append(dataframe['rsi'] > 70)
|
||||
|
||||
# 条件3: MACD 下降
|
||||
conditions.append(dataframe['macd'] > dataframe['macd_signal'])
|
||||
|
||||
# 至少满足 2 个条件
|
||||
condition_count = conditions[0].astype(int) + conditions[1].astype(int) + conditions[2].astype(int)
|
||||
final_signal = condition_count >= 2
|
||||
|
||||
dataframe.loc[final_signal, 'exit_long'] = 1
|
||||
|
||||
return dataframe
|
||||
56
user_data/strategies/freqaiprimer.json
Normal file
56
user_data/strategies/freqaiprimer.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"strategy_name": "FreqaiPrimer",
|
||||
"params": {
|
||||
"roi": {},
|
||||
"stoploss": {
|
||||
"stoploss": -0.085
|
||||
},
|
||||
"trailing": {
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.0125,
|
||||
"trailing_stop_positive_offset": 0.045,
|
||||
"trailing_only_offset_is_reached": false
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
},
|
||||
"buy": {
|
||||
"add_bb_lower_proximity": 1.102,
|
||||
"add_position_callback": 0.052,
|
||||
"add_position_decrease_ratio": 0.36,
|
||||
"add_rsi_oversold_threshold": 31,
|
||||
"add_stochrsi_oversold": 21,
|
||||
"adjust_multiplier": 0.66,
|
||||
"bb_length": 36,
|
||||
"bb_lower_deviation": 0.926,
|
||||
"bb_std": 2.7,
|
||||
"bb_width_threshold": 0.005,
|
||||
"h1_max_candles": 40,
|
||||
"h1_max_consecutive_candles": 2,
|
||||
"h1_rapid_rise_threshold": 0.08,
|
||||
"max_entry_adjustments": 6,
|
||||
"min_condition_count": 2,
|
||||
"rsi_bull_threshold": 68,
|
||||
"rsi_length": 14,
|
||||
"rsi_oversold": 20,
|
||||
"stochrsi_bull_threshold": 21,
|
||||
"stochrsi_neutral_threshold": 22,
|
||||
"volume_multiplier": 4.3
|
||||
},
|
||||
"sell": {
|
||||
"exit_bb_upper_deviation": 0.923,
|
||||
"exit_profit_tier1": 0.032,
|
||||
"exit_profit_tier2": 0.167,
|
||||
"exit_reduce_tier1": 0.69,
|
||||
"exit_reduce_tier2": 0.33,
|
||||
"exit_rsi_threshold": 68,
|
||||
"exit_volume_multiplier": 5.1,
|
||||
"max_reduce_adjustments": 4,
|
||||
"reduce_coefficient": 0.263,
|
||||
"reduce_profit_base": 0.063
|
||||
},
|
||||
"protection": {}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-11-23 14:25:38.989402+00:00"
|
||||
}
|
||||
35
user_data/strategies/freqaiprimer.json.orig
Normal file
35
user_data/strategies/freqaiprimer.json.orig
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"strategy_name": "FreqaiPrimer",
|
||||
"params": {
|
||||
"roi": {},
|
||||
"stoploss": {
|
||||
"stoploss": -0.05
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
},
|
||||
"buy": {
|
||||
"ADD_POSITION_THRESHOLD": -0.021,
|
||||
"BUY_THRESHOLD_MAX": -0.001,
|
||||
"BUY_THRESHOLD_MIN": -0.035,
|
||||
"COOLDOWN_PERIOD_MINUTES": 9,
|
||||
"MAX_ENTRY_POSITION_ADJUSTMENT": 3
|
||||
},
|
||||
"sell": {
|
||||
"EXIT_POSITION_RATIO": 0.472,
|
||||
"SELL_THRESHOLD_MAX": 0.065,
|
||||
"SELL_THRESHOLD_MIN": 0.002,
|
||||
"TRAILING_STOP_DISTANCE": 0.015,
|
||||
"TRAILING_STOP_START": 0.016
|
||||
},
|
||||
"protection": {},
|
||||
"trailing": {
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.106,
|
||||
"trailing_stop_positive_offset": 0.196,
|
||||
"trailing_only_offset_is_reached": false
|
||||
}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-07-01 14:51:29.420394+00:00"
|
||||
}
|
||||
756
user_data/strategies/freqaiprimer.py
Normal file
756
user_data/strategies/freqaiprimer.py
Normal file
@ -0,0 +1,756 @@
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="pandas_ta")
|
||||
|
||||
import logging
|
||||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||||
from pandas import DataFrame
|
||||
import pandas_ta as ta
|
||||
from freqtrade.persistence import Trade
|
||||
import numpy as np
|
||||
import datetime
|
||||
import pandas as pd
|
||||
import math
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FreqaiPrimer(IStrategy):
|
||||
# 策略参数 - 使用custom_roi替代minimal_roi字典
|
||||
loglevel = "warning"
|
||||
|
||||
# 启用自定义ROI回调函数
|
||||
use_custom_roi = True
|
||||
|
||||
# 写死在策略里,永远不参与 hyperopt
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.012 # 1.2% 固定回调(震荡市最稳)
|
||||
trailing_stop_positive_offset = 0.02 # 1.8% 盈利后才激活(防止过早启动)
|
||||
trailing_only_offset_is_reached = True # 必须等盈利超过 offset 才启动 trailing
|
||||
|
||||
# 用于跟踪市场状态的数据框缓存
|
||||
_dataframe_cache = None
|
||||
|
||||
def __init__(self, config=None):
|
||||
"""初始化策略参数,调用父类初始化方法并接受config参数"""
|
||||
super().__init__(config) # 调用父类的初始化方法并传递config
|
||||
assert self.h1_max_candles.value <= 50, f"h1_max_candles={self.h1_max_candles.value} 超出安全范围!"
|
||||
|
||||
@property
|
||||
def protections(self):
|
||||
"""
|
||||
保护机制配置
|
||||
基于最新Freqtrade规范,保护机制应定义在策略文件中而非配置文件
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 60, # 3小时回看期(60根3分钟K线)
|
||||
"trade_limit": 2, # 最多2笔止损交易
|
||||
"stop_duration_candles": 60, # 暂停180分钟(60根3分钟K线)
|
||||
"only_per_pair": False # 仅针对单个币对
|
||||
},
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": 18 # 6分钟冷却期(2根3分钟K线)
|
||||
},
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"lookback_period_candles": 48, # 2.4小时回看期
|
||||
"trade_limit": 4, # 4笔交易限制
|
||||
"stop_duration_candles": 24, # 72分钟暂停(24根3分钟K线)
|
||||
"max_allowed_drawdown": 0.20 # 20%最大回撤容忍度
|
||||
}
|
||||
]
|
||||
|
||||
@property
|
||||
def trailing_stop_positive(self):
|
||||
"""根据市场状态动态调整跟踪止盈参数"""
|
||||
# 获取当前市场状态
|
||||
if self._dataframe_cache is not None and len(self._dataframe_cache) > 0:
|
||||
current_state = self._dataframe_cache['market_state'].iloc[-1]
|
||||
if current_state == 'strong_bull':
|
||||
return 0.007 # 强劲牛市中降低跟踪止盈,让利润奔跑
|
||||
elif current_state == 'weak_bull':
|
||||
return 0.005 # 弱势牛市中保持较低的跟踪止盈
|
||||
return self._trailing_stop_positive_default # 返回默认值
|
||||
|
||||
@trailing_stop_positive.setter
|
||||
def trailing_stop_positive(self, value):
|
||||
"""设置trailing_stop_positive的默认值"""
|
||||
self._trailing_stop_positive_default = value
|
||||
|
||||
timeframe = "3m" # 主时间框架为 3 分钟
|
||||
can_short = False # 禁用做空
|
||||
|
||||
# [propertiesGrp_List]--------------------------------------------------------------------------------------------------------------------------------------
|
||||
# [propertiesGrp step="1" name="第一轮优化" epochs="160" space="buy " description="入场基础条件优化,入场确认条件优化"]
|
||||
bb_std = DecimalParameter(2.0, 5.0, decimals=1, default=2.7, optimize=True, load=True, space='buy') # 安全:2.0-5.0
|
||||
rsi_length = IntParameter(10, 30, default=14, optimize=True, load=True, space='buy') # 安全:10-30
|
||||
bb_lower_deviation = DecimalParameter(0.92, 1.15, decimals=3, default=0.926, optimize=True, load=True, space='buy') # 安全:0.92-1.15
|
||||
stochrsi_bull_threshold = IntParameter(20, 50, default=21, optimize=True, load=True, space='buy') # 安全:20-50
|
||||
volume_multiplier = DecimalParameter(1.5, 6.0, decimals=1, default=4.3, optimize=True, load=True, space='buy') # 安全:1.5-6.0
|
||||
min_condition_count = IntParameter(1, 2, default=2, optimize=True, load=True, space='buy') # 最多只允许2个条件!
|
||||
bb_length = IntParameter(20, 60, default=36, optimize=True, load=True, space='buy') # 安全:20-60
|
||||
# [/propertiesGrp]
|
||||
|
||||
# [propertiesGrp step="2" name="第二轮优化 - 剧烈拉升检测" epochs="160" space="buy" description="防追高核心参数,绝对不能放宽!"]
|
||||
rsi_oversold = IntParameter(20, 50, default=20, optimize=True, load=True, space='buy') # 安全:20-50
|
||||
rsi_bull_threshold = IntParameter(40, 68, default=68, optimize=True, load=True, space='buy') # 安全:40-68
|
||||
stochrsi_neutral_threshold = IntParameter(15, 40, default=22, optimize=True, load=True, space='buy') # 安全:15-40
|
||||
bb_width_threshold = DecimalParameter(0.003, 0.030, decimals=3, default=0.005, optimize=True, load=True, space='buy') # 安全:0.003-0.030
|
||||
h1_max_candles = IntParameter(16, 50, default=40, optimize=True, load=True, space='buy') # 黄金区间!绝不能超过50
|
||||
h1_rapid_rise_threshold = DecimalParameter(0.08, 0.22, decimals=3, default=0.08, optimize=True, load=True, space='buy') # 0.08-0.22 实盘最稳
|
||||
h1_max_consecutive_candles = IntParameter(1, 2, default=2, optimize=True, load=True, space='buy') # 固定为1最稳,2也行
|
||||
# [/propertiesGrp]
|
||||
|
||||
# [propertiesGrp step="3" name="第三轮优化 - 加仓策略" epochs="160" space="buy" description="加仓精准度与金额管理,严防爆仓"]
|
||||
add_position_callback = DecimalParameter(0.025, 0.070, decimals=3, default=0.052, optimize=True, load=True, space='buy') # 2.5%-7.0% 回调才加
|
||||
add_rsi_oversold_threshold = IntParameter(15, 40, default=20, optimize=True, load=True, space='buy') # 不能太低
|
||||
add_stochrsi_oversold = IntParameter(10, 35, default=20, optimize=True, load=True, space='buy')
|
||||
add_bb_lower_proximity = DecimalParameter(0.85, 1.20, decimals=3, default=1.102, optimize=True, load=True, space='buy') # 不能离下轨太远
|
||||
add_position_decrease_ratio= DecimalParameter(0.30, 0.80, decimals=2, default=0.36, optimize=True, load=True, space='buy') # 递减比例别太激进
|
||||
max_entry_adjustments = IntParameter(2, 7, default=6, optimize=True, load=True, space='buy') # 最多7次加仓,防爆仓
|
||||
adjust_multiplier = DecimalParameter(0.6, 1.6, decimals=2, default=0.66, optimize=True, load=True, space='buy') # 别让加仓金额指数爆炸
|
||||
# [/propertiesGrp]
|
||||
|
||||
# [propertiesGrp step="4" name="第四轮优化 - 出场与分级止盈" epochs="200" space="sell" description="出场条件与分级止盈,减仓与风险管理"]
|
||||
exit_bb_upper_deviation = DecimalParameter(0.90, 1.15, decimals=3, default=0.923, optimize=True, load=True, space='sell')
|
||||
exit_volume_multiplier = DecimalParameter(2.0, 7.0, decimals=1, default=5.1, optimize=True, load=True, space='sell')
|
||||
exit_rsi_threshold = IntParameter(55, 72, default=68, optimize=True, load=True, space='sell') # 牛市也能出得了场
|
||||
exit_profit_tier1 = DecimalParameter(0.03, 0.12, decimals=3, default=0.032, optimize=True, load=True, space='sell')
|
||||
exit_reduce_tier1 = DecimalParameter(0.20, 0.70, decimals=2, default=0.69, optimize=True, load=True, space='sell')
|
||||
exit_profit_tier2 = DecimalParameter(0.08, 0.20, decimals=3, default=0.167, optimize=True, load=True, space='sell')
|
||||
exit_reduce_tier2 = DecimalParameter(0.15, 0.60, decimals=2, default=0.33, optimize=True, load=True, space='sell')
|
||||
reduce_profit_base = DecimalParameter(0.02, 0.12, decimals=3, default=0.063, optimize=True, load=True, space='sell')
|
||||
reduce_coefficient = DecimalParameter(0.15, 0.55, decimals=3, default=0.263, optimize=True, load=True, space='sell')
|
||||
max_reduce_adjustments = IntParameter(1, 4, default=4, optimize=True, load=True, space='sell') # 最多4次减仓就够了
|
||||
# [/propertiesGrp]
|
||||
|
||||
# [propertiesGrp step="5" name="第五轮优化" epochs="80" space="roi stoploss" description="最终ROI与止损微调"]
|
||||
# (这里可以放你后续要优化的ROI表、动态止损系数等)
|
||||
# [/propertiesGrp]
|
||||
# [/propertiesGrp_List]-----------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def informative_pairs(self):
|
||||
pairs = self.dp.current_whitelist()
|
||||
return [(pair, '15m') for pair in pairs] + [(pair, '1h') for pair in pairs]
|
||||
|
||||
def _validate_dataframe_columns(self, dataframe: DataFrame, required_columns: list, metadata: dict):
|
||||
"""
|
||||
验证数据框中是否包含所有需要的列。
|
||||
如果缺少列,则记录警告日志。
|
||||
"""
|
||||
missing_columns = [col for col in required_columns if col not in dataframe.columns]
|
||||
if missing_columns:
|
||||
logger.warning(f"[{metadata['pair']}] 数据框中缺少以下列: {missing_columns}")
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: pd.Timestamp, proposed_rate: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
"""
|
||||
自定义入场价格:给入场价格打98折(降低2%)
|
||||
添加零值保护,防止除零错误
|
||||
"""
|
||||
# 零值保护:如果proposed_rate为0或异常小值,直接返回原值
|
||||
if proposed_rate <= 0 or proposed_rate < 1e-8:
|
||||
logger.warning(f"[{pair}] proposed_rate异常: {proposed_rate},返回原值")
|
||||
return proposed_rate if proposed_rate > 0 else 0.0
|
||||
|
||||
# 入场价格折扣:98折(降低2%)
|
||||
discounted_rate = proposed_rate * 0.995
|
||||
return discounted_rate
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: pd.Timestamp,
|
||||
current_rate: float,
|
||||
proposed_stake: float,
|
||||
min_stake: float,
|
||||
max_stake: float,
|
||||
**kwargs) -> float:
|
||||
# 获取初始资金(回测中固定为dry_run_wallet的值)
|
||||
initial_balance = self.config.get('dry_run_wallet', 10000)
|
||||
# 始终以初始资金的3.75%计算
|
||||
desired_stake = initial_balance * 0.0375
|
||||
desired_stake = math.floor(desired_stake) # 取整,去掉小数点后的数字
|
||||
return max(min(desired_stake, max_stake), min_stake)
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 计算 3m 周期的指标
|
||||
bb_length_value = self.bb_length.value
|
||||
bb_std_value = self.bb_std.value
|
||||
rsi_length_value = self.rsi_length.value
|
||||
|
||||
bb_3m = ta.bbands(dataframe['close'], length=bb_length_value, std=bb_std_value)
|
||||
dataframe['bb_lower_3m'] = bb_3m[f'BBL_{bb_length_value}_{bb_std_value}']
|
||||
dataframe['bb_upper_3m'] = bb_3m[f'BBU_{bb_length_value}_{bb_std_value}']
|
||||
dataframe['rsi_3m'] = ta.rsi(dataframe['close'], length=rsi_length_value)
|
||||
|
||||
# 新增 StochRSI 指标
|
||||
stochrsi_3m = ta.stochrsi(dataframe['close'], length=rsi_length_value, rsi_length=rsi_length_value)
|
||||
dataframe['stochrsi_k_3m'] = stochrsi_3m[f'STOCHRSIk_{rsi_length_value}_{rsi_length_value}_3_3']
|
||||
dataframe['stochrsi_d_3m'] = stochrsi_3m[f'STOCHRSId_{rsi_length_value}_{rsi_length_value}_3_3']
|
||||
|
||||
# 新增 MACD 指标
|
||||
macd_3m = ta.macd(dataframe['close'], fast=12, slow=26, signal=9)
|
||||
dataframe['macd_3m'] = macd_3m['MACD_12_26_9']
|
||||
dataframe['macd_signal_3m'] = macd_3m['MACDs_12_26_9']
|
||||
dataframe['macd_hist_3m'] = macd_3m['MACDh_12_26_9']
|
||||
|
||||
# 计算3m时间框架的EMA50和EMA200
|
||||
dataframe['ema_50_3m'] = ta.ema(dataframe['close'], length=50)
|
||||
dataframe['ema_200_3m'] = ta.ema(dataframe['close'], length=200)
|
||||
|
||||
# 成交量过滤
|
||||
dataframe['volume_ma'] = dataframe['volume'].rolling(20).mean()
|
||||
|
||||
# 计算 ATR 用于动态止损和退出
|
||||
dataframe['atr'] = ta.atr(dataframe['high'], dataframe['low'], dataframe['close'], length=14)
|
||||
|
||||
# 获取 15m 数据
|
||||
df_15m = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='15m')
|
||||
df_15m['rsi_15m'] = ta.rsi(df_15m['close'], length=rsi_length_value)
|
||||
# 计算15m时间框架的EMA50和EMA200
|
||||
df_15m['ema_50_15m'] = ta.ema(df_15m['close'], length=50)
|
||||
df_15m['ema_200_15m'] = ta.ema(df_15m['close'], length=200)
|
||||
|
||||
# 新增 StochRSI 指标
|
||||
stochrsi_15m = ta.stochrsi(df_15m['close'], length=rsi_length_value, rsi_length=rsi_length_value)
|
||||
df_15m['stochrsi_k_15m'] = stochrsi_15m[f'STOCHRSIk_{rsi_length_value}_{rsi_length_value}_3_3']
|
||||
df_15m['stochrsi_d_15m'] = stochrsi_15m[f'STOCHRSId_{rsi_length_value}_{rsi_length_value}_3_3']
|
||||
|
||||
# 新增 MACD 指标
|
||||
macd_15m = ta.macd(df_15m['close'], fast=12, slow=26, signal=9)
|
||||
df_15m['macd_15m'] = macd_15m['MACD_12_26_9']
|
||||
df_15m['macd_signal_15m'] = macd_15m['MACDs_12_26_9']
|
||||
df_15m['macd_hist_15m'] = macd_15m['MACDh_12_26_9']
|
||||
|
||||
# 将 15m 数据重新索引到主时间框架 (3m)
|
||||
df_15m = df_15m.set_index('date').reindex(dataframe['date']).reset_index()
|
||||
df_15m = df_15m.rename(columns={'index': 'date'})
|
||||
df_15m = df_15m[['date', 'rsi_15m', 'ema_50_15m', 'ema_200_15m']].ffill()
|
||||
|
||||
# 合并 15m 数据
|
||||
dataframe = dataframe.merge(df_15m, how='left', on='date')
|
||||
|
||||
# 获取 1h 数据
|
||||
df_1h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1h')
|
||||
|
||||
# 计算 1h 布林带
|
||||
bb_1h = ta.bbands(df_1h['close'], length=bb_length_value, std=bb_std_value)
|
||||
df_1h['bb_lower_1h'] = bb_1h[f'BBL_{bb_length_value}_{bb_std_value}']
|
||||
df_1h['bb_upper_1h'] = bb_1h[f'BBU_{bb_length_value}_{bb_std_value}']
|
||||
|
||||
# 计算 1h RSI 和 EMA
|
||||
df_1h['rsi_1h'] = ta.rsi(df_1h['close'], length=rsi_length_value)
|
||||
df_1h['ema_50_1h'] = ta.ema(df_1h['close'], length=50) # 1h 50周期EMA
|
||||
df_1h['ema_200_1h'] = ta.ema(df_1h['close'], length=200) # 1h 200周期EMA
|
||||
df_1h['trend_1h'] = df_1h['close'] > df_1h['ema_50_1h'] # 1h上涨趋势
|
||||
|
||||
# 新增 StochRSI 指标
|
||||
stochrsi_1h = ta.stochrsi(df_1h['close'], length=rsi_length_value, rsi_length=rsi_length_value)
|
||||
df_1h['stochrsi_k_1h'] = stochrsi_1h[f'STOCHRSIk_{rsi_length_value}_{rsi_length_value}_3_3']
|
||||
df_1h['stochrsi_d_1h'] = stochrsi_1h[f'STOCHRSId_{rsi_length_value}_{rsi_length_value}_3_3']
|
||||
|
||||
# 新增 MACD 指标
|
||||
macd_1h = ta.macd(df_1h['close'], fast=12, slow=26, signal=9)
|
||||
df_1h['macd_1h'] = macd_1h['MACD_12_26_9']
|
||||
df_1h['macd_signal_1h'] = macd_1h['MACDs_12_26_9']
|
||||
df_1h['macd_hist_1h'] = macd_1h['MACDh_12_26_9']
|
||||
|
||||
# 验证 MACD 列是否正确生成
|
||||
#logger.info(f"[{metadata['pair']}] 1小时 MACD 列: {list(macd_1h.columns)}")
|
||||
|
||||
# 确保 StochRSI 指标已正确计算
|
||||
# 将 1h 数据重新索引到主时间框架 (3m),并填充缺失值
|
||||
df_1h = df_1h.set_index('date').reindex(dataframe['date']).ffill().bfill().reset_index()
|
||||
df_1h = df_1h.rename(columns={'index': 'date'})
|
||||
# Include macd_1h and macd_signal_1h in the column selection
|
||||
df_1h = df_1h[['date', 'rsi_1h', 'trend_1h', 'ema_50_1h', 'ema_200_1h', 'bb_lower_1h', 'bb_upper_1h', 'stochrsi_k_1h', 'stochrsi_d_1h', 'macd_1h', 'macd_signal_1h']].ffill()
|
||||
|
||||
# Validate that all required columns are present
|
||||
required_columns = ['date', 'rsi_1h', 'trend_1h', 'ema_50_1h', 'ema_200_1h',
|
||||
'bb_lower_1h', 'bb_upper_1h', 'stochrsi_k_1h', 'stochrsi_d_1h',
|
||||
'macd_1h', 'macd_signal_1h']
|
||||
missing_columns = [col for col in required_columns if col not in df_1h.columns]
|
||||
if missing_columns:
|
||||
logger.error(f"[{metadata['pair']}] 缺少以下列: {missing_columns}")
|
||||
raise KeyError(f"缺少以下列: {missing_columns}")
|
||||
|
||||
# 确保所有需要的列都被合并
|
||||
required_columns = ['date', 'rsi_1h', 'trend_1h', 'ema_50_1h', 'ema_200_1h',
|
||||
'bb_lower_1h', 'bb_upper_1h', 'stochrsi_k_1h', 'stochrsi_d_1h',
|
||||
'macd_1h', 'macd_signal_1h']
|
||||
|
||||
# 验证所需列是否存在
|
||||
missing_columns = [col for col in required_columns if col not in df_1h.columns]
|
||||
if missing_columns:
|
||||
logger.error(f"[{metadata['pair']}] 缺少以下列: {missing_columns}")
|
||||
raise KeyError(f"缺少以下列: {missing_columns}")
|
||||
|
||||
df_1h = df_1h[required_columns] # 确保包含 macd_1h 和 macd_signal_1h
|
||||
|
||||
# 合并 1h 数据
|
||||
dataframe = dataframe.merge(df_1h, how='left', on='date').ffill()
|
||||
|
||||
# 验证合并后的列
|
||||
#logger.info(f"[{metadata['pair']}] 合并后的数据框列名: {list(dataframe.columns)}")
|
||||
|
||||
# K线形态:看涨吞没
|
||||
dataframe['bullish_engulfing'] = (
|
||||
(dataframe['close'].shift(1) < dataframe['open'].shift(1)) &
|
||||
(dataframe['close'] > dataframe['open']) &
|
||||
(dataframe['close'] > dataframe['open'].shift(1)) &
|
||||
(dataframe['open'] < dataframe['close'].shift(1))
|
||||
)
|
||||
|
||||
# 计算各时间框架的趋势状态(牛/熊)
|
||||
# 3m时间框架:ema50下穿ema200为熊,上穿为牛
|
||||
dataframe['trend_3m'] = np.where(dataframe['ema_50_3m'] > dataframe['ema_200_3m'], 1, 0)
|
||||
|
||||
# 15m时间框架:ema50下穿ema200为熊,上穿为牛
|
||||
dataframe['trend_15m'] = np.where(dataframe['ema_50_15m'] > dataframe['ema_200_15m'], 1, 0)
|
||||
|
||||
# 1h时间框架:ema50下穿ema200为熊,上穿为牛
|
||||
dataframe['trend_1h_ema'] = np.where(dataframe['ema_50_1h'] > dataframe['ema_200_1h'], 1, 0)
|
||||
|
||||
# 计算熊牛得分(0-100)
|
||||
# 权重:3m熊牛权重10,15m熊牛权重35,1h熊牛权重65
|
||||
# 计算加权得分
|
||||
dataframe['market_score'] = (
|
||||
dataframe['trend_3m'] * 10 +
|
||||
dataframe['trend_15m'] * 35 +
|
||||
dataframe['trend_1h_ema'] * 65
|
||||
)
|
||||
|
||||
# 确保得分在0-100范围内
|
||||
dataframe['market_score'] = dataframe['market_score'].clip(lower=0, upper=100)
|
||||
|
||||
# 根据得分分类市场状态
|
||||
dataframe['market_state'] = 'neutral'
|
||||
dataframe.loc[dataframe['market_score'] > 70, 'market_state'] = 'strong_bull'
|
||||
dataframe.loc[(dataframe['market_score'] > 50) & (dataframe['market_score'] <= 70), 'market_state'] = 'weak_bull'
|
||||
dataframe.loc[(dataframe['market_score'] >= 30) & (dataframe['market_score'] <= 50), 'market_state'] = 'neutral'
|
||||
dataframe.loc[(dataframe['market_score'] > 10) & (dataframe['market_score'] < 30), 'market_state'] = 'weak_bear'
|
||||
dataframe.loc[dataframe['market_score'] <= 10, 'market_state'] = 'strong_bear'
|
||||
|
||||
# 创建一个使用前一行市场状态的列,避免在populate_entry_trend中使用iloc[-1]
|
||||
dataframe['prev_market_state'] = dataframe['market_state'].shift(1)
|
||||
# 为第一行设置默认值
|
||||
dataframe['prev_market_state'] = dataframe['prev_market_state'].fillna('neutral')
|
||||
|
||||
# 记录当前的市场状态
|
||||
# if len(dataframe) > 0:
|
||||
# current_score = dataframe['market_score'].iloc[-1]
|
||||
# current_state = dataframe['market_state'].iloc[-1]
|
||||
#logger.info(f"[{metadata['pair']}] 熊牛得分: {current_score:.1f}, 市场状态: {current_state}")
|
||||
#logger.info(f"[{metadata['pair']}] 各时间框架趋势: 3m={'牛' if dataframe['trend_3m'].iloc[-1] == 1 else '熊'}, \
|
||||
# 15m={'牛' if dataframe['trend_15m'].iloc[-1] == 1 else '熊'}, \
|
||||
# 1h={'牛' if dataframe['trend_1h_ema'].iloc[-1] == 1 else '熊'}")
|
||||
|
||||
# 调试:打印指标值(最后 5 行),验证时间对齐
|
||||
#print(f"Pair: {metadata['pair']}, Last 5 rows after reindexing:")
|
||||
#print(dataframe[['date', 'close', 'bb_lower_3m', 'rsi_3m', 'rsi_15m', 'rsi_1h', 'trend_1h',
|
||||
# 'trend_3m', 'trend_15m', 'trend_1h_ema', 'market_score', 'market_state',
|
||||
# 'bullish_engulfing', 'volume', 'volume_ma']].tail(5))
|
||||
|
||||
# 打印最终数据框的列名以验证
|
||||
#logger.info(f"[{metadata['pair']}] 最终数据框列名: {list(dataframe.columns)}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 出场信号基于趋势和量价关系
|
||||
# 确保market_state列存在
|
||||
if 'market_state' not in dataframe.columns:
|
||||
dataframe['market_state'] = 'neutral'
|
||||
|
||||
# ======================== 巨不下器的出场逻辑 ========================
|
||||
# 极其区慢,给利润奔跑的机会
|
||||
|
||||
# 条件1: RSI极度超买(大于75才考虑出场)
|
||||
rsi_extreme_overbought = dataframe['rsi_1h'] > 75
|
||||
|
||||
# 条件2: 趋势大幅反转(超卖)
|
||||
trend_extreme_reversal = dataframe['rsi_1h'] < 20
|
||||
|
||||
# 条件3: MACD死亡交叉(严格的反转信号)
|
||||
macd_death_cross = (
|
||||
(dataframe['macd_1h'] < dataframe['macd_signal_1h']) &
|
||||
(dataframe['macd_1h'].shift(1) >= dataframe['macd_signal_1h'].shift(1))
|
||||
)
|
||||
|
||||
# 综合成出条件(需要需要偏保守,不然遍地开花)
|
||||
final_condition = (
|
||||
rsi_extreme_overbought |
|
||||
trend_extreme_reversal |
|
||||
macd_death_cross
|
||||
)
|
||||
|
||||
# 设置出场信号
|
||||
dataframe.loc[final_condition, 'exit_long'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 确保prev_market_state列存在
|
||||
if 'prev_market_state' not in dataframe.columns:
|
||||
dataframe['prev_market_state'] = 'neutral'
|
||||
|
||||
# ======================== 简化版入场逻辑 - 只需1个条件满足 ========================
|
||||
|
||||
# 条件1: RSI超卖(核心条件)
|
||||
rsi_oversold = dataframe['rsi_1h'] < self.rsi_oversold.value
|
||||
|
||||
# 条件2: 价格接近BB下轨
|
||||
price_near_bb_lower = dataframe['close'] <= dataframe['bb_lower_1h'] * self.bb_lower_deviation.value
|
||||
|
||||
# 条件3: MACD看涨
|
||||
macd_bullish = dataframe['macd_1h'] > dataframe['macd_signal_1h']
|
||||
|
||||
# 条件4: 避免极高位
|
||||
not_too_high = dataframe['close'] < dataframe['bb_upper_1h']
|
||||
|
||||
# 只需满足任意1个条件就入场(极其宽松)
|
||||
final_condition = (
|
||||
rsi_oversold |
|
||||
price_near_bb_lower |
|
||||
macd_bullish
|
||||
) & not_too_high # 但必须不在极高位
|
||||
|
||||
# 设置入场信号
|
||||
dataframe.loc[final_condition, 'enter_long'] = 1
|
||||
|
||||
# 日志
|
||||
signal_count = dataframe['enter_long'].sum()
|
||||
if signal_count > 0:
|
||||
logger.info(f"[{metadata['pair']}] 🎯 发现 {signal_count} 个入场信号")
|
||||
|
||||
return dataframe
|
||||
|
||||
def detect_h1_rapid_rise(self, pair: str) -> bool:
|
||||
"""
|
||||
检测1小时K线图上的剧烈拉升情况(轻量级版本,用于confirm_trade_entry)
|
||||
参数:
|
||||
- pair: 交易对
|
||||
返回:
|
||||
- bool: 是否处于不稳固区域
|
||||
"""
|
||||
try:
|
||||
# 获取1小时K线数据
|
||||
df_1h = self.dp.get_pair_dataframe(pair=pair, timeframe='1h')
|
||||
|
||||
# 获取当前优化参数值
|
||||
max_candles = self.h1_max_candles.value
|
||||
rapid_rise_threshold = self.h1_rapid_rise_threshold.value
|
||||
max_consecutive_candles = self.h1_max_consecutive_candles.value
|
||||
|
||||
# 确保有足够的K线数据
|
||||
if len(df_1h) < max_candles:
|
||||
logger.warning(f"[{pair}] 1h K线数据不足 {max_candles} 根,当前只有 {len(df_1h)} 根,无法完整检测剧烈拉升")
|
||||
return False
|
||||
|
||||
# 获取最近的K线
|
||||
recent_data = df_1h.iloc[-max_candles:].copy()
|
||||
|
||||
# 检查连续最多几根K线内的最大涨幅
|
||||
rapid_rise_detected = False
|
||||
max_rise = 0
|
||||
|
||||
for i in range(len(recent_data) - max_consecutive_candles + 1):
|
||||
window_data = recent_data.iloc[i:i + max_consecutive_candles]
|
||||
window_low = window_data['low'].min()
|
||||
window_high = window_data['high'].max()
|
||||
|
||||
# 计算区间内的最大涨幅
|
||||
if window_low > 0:
|
||||
rise_percentage = (window_high - window_low) / window_low
|
||||
if rise_percentage > max_rise:
|
||||
max_rise = rise_percentage
|
||||
|
||||
# 检查是否超过阈值
|
||||
if rise_percentage >= rapid_rise_threshold:
|
||||
rapid_rise_detected = True
|
||||
#logger.info(f"[{pair}] 检测到剧烈拉升: 从 {window_low:.2f} 到 {window_high:.2f} ({rise_percentage:.2%}) 在 {max_consecutive_candles} 根K线内")
|
||||
break
|
||||
|
||||
current_price = recent_data['close'].iloc[-1]
|
||||
#logger.info(f"[{pair}] 剧烈拉升检测结果: {'不稳固' if rapid_rise_detected else '稳固'}")
|
||||
#logger.info(f"[{pair}] 最近最大涨幅: {max_rise:.2%}")
|
||||
|
||||
return rapid_rise_detected
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{pair}] 剧烈拉升检测过程中发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def confirm_trade_entry(
|
||||
self,
|
||||
pair: str,
|
||||
order_type: str,
|
||||
amount: float,
|
||||
rate: float,
|
||||
time_in_force: str,
|
||||
current_time: datetime,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
"""
|
||||
交易买入前的确认函数
|
||||
⚠️ 禁用剧烈拉升检测以增加交易频率
|
||||
"""
|
||||
# 允许所有符合条件的交易
|
||||
return True
|
||||
|
||||
def _check_add_position_conditions(self, pair: str, current_rate: float, current_profit: float,
|
||||
entry_count: int, initial_price: float, dataframe) -> dict:
|
||||
"""
|
||||
检查加仓条件的多维度评分系统
|
||||
返回: {'should_add': bool, 'score': float, 'reasons': list}
|
||||
"""
|
||||
try:
|
||||
if dataframe is None or len(dataframe) < 30:
|
||||
return {'should_add': False, 'score': 0, 'reasons': ['数据不足']}
|
||||
|
||||
last_candle = dataframe.iloc[-1]
|
||||
reasons = []
|
||||
score = 0.0
|
||||
max_score = 6.0
|
||||
|
||||
# 条件1:跌幅确认(基础条件,必须满足)
|
||||
price_diff_pct = (current_rate - initial_price) / initial_price
|
||||
callback_threshold = -self.add_position_callback.value
|
||||
|
||||
if price_diff_pct <= callback_threshold:
|
||||
score += 1.0
|
||||
reasons.append(f"✓ 跌幅{price_diff_pct:.2%}≤{callback_threshold:.2%}")
|
||||
else:
|
||||
return {'should_add': False, 'score': 0, 'reasons': [f'✗ 跌幅不足: {price_diff_pct:.2%} > {callback_threshold:.2%}']}
|
||||
|
||||
# 条件2:RSI超卖确认
|
||||
rsi_1h = last_candle.get('rsi_1h', 50)
|
||||
if rsi_1h < self.add_rsi_oversold_threshold.value:
|
||||
score += 1.0
|
||||
reasons.append(f"✓ RSI超卖: {rsi_1h:.1f} < {self.add_rsi_oversold_threshold.value}")
|
||||
else:
|
||||
reasons.append(f"✗ RSI不超卖: {rsi_1h:.1f} ≥ {self.add_rsi_oversold_threshold.value}")
|
||||
|
||||
# 条件3:StochRSI双线低位确认
|
||||
stochrsi_k = last_candle.get('stochrsi_k_1h', 50)
|
||||
stochrsi_d = last_candle.get('stochrsi_d_1h', 50)
|
||||
if stochrsi_k < self.add_stochrsi_oversold.value and stochrsi_d < self.add_stochrsi_oversold.value:
|
||||
score += 1.0
|
||||
reasons.append(f"✓ StochRSI双超卖: K={stochrsi_k:.1f}, D={stochrsi_d:.1f}")
|
||||
else:
|
||||
reasons.append(f"✗ StochRSI未双超卖: K={stochrsi_k:.1f}, D={stochrsi_d:.1f}")
|
||||
|
||||
# 条件4:MACD上升确认(底部反转信号)
|
||||
macd_1h = last_candle.get('macd_1h', 0)
|
||||
macd_signal_1h = last_candle.get('macd_signal_1h', 0)
|
||||
macd_hist = macd_1h - macd_signal_1h
|
||||
|
||||
if len(dataframe) >= 2:
|
||||
prev_macd_hist = dataframe.iloc[-2].get('macd_1h', 0) - dataframe.iloc[-2].get('macd_signal_1h', 0)
|
||||
if macd_hist > prev_macd_hist: # 简化条件,只检查MACD柱值上升
|
||||
score += 1.0
|
||||
reasons.append(f"✓ MACD底部上升: 柱值={macd_hist:.6f}")
|
||||
else:
|
||||
reasons.append(f"✗ MACD未确认: 柱值={macd_hist:.6f}")
|
||||
|
||||
# 条件5:布林带下轨支撑确认
|
||||
bb_lower = last_candle.get('bb_lower_1h', current_rate)
|
||||
bb_proximity_ratio = current_rate / bb_lower if bb_lower > 0 else 1.0
|
||||
|
||||
if bb_proximity_ratio <= self.add_bb_lower_proximity.value:
|
||||
score += 1.0
|
||||
reasons.append(f"✓ 接近BB下轨: 比例={bb_proximity_ratio:.4f}")
|
||||
else:
|
||||
reasons.append(f"✗ 离BB下轨太远: 比例={bb_proximity_ratio:.4f}")
|
||||
|
||||
# 条件6:成交量放大确认(简化条件)
|
||||
volume = last_candle.get('volume', 0)
|
||||
volume_ma = last_candle.get('volume_ma', 1)
|
||||
if volume > volume_ma * 1.2: # 固定1.2倍成交量确认
|
||||
score += 1.0
|
||||
reasons.append(f"✓ 成交量放大: {volume:.0f} > {volume_ma * 1.2:.0f}")
|
||||
else:
|
||||
reasons.append(f"✗ 成交量不足: {volume:.0f} ≤ {volume_ma * 1.2:.0f}")
|
||||
|
||||
# 条件7:市场状态过滤(强熊市禁止加仓)
|
||||
market_state = last_candle.get('market_state', 'neutral')
|
||||
if market_state != 'strong_bear':
|
||||
score += 0.5
|
||||
reasons.append(f"✓ 市场状态良好: {market_state}")
|
||||
else:
|
||||
reasons.append(f"✗ 强熊市,避免加仓: {market_state}")
|
||||
return {'should_add': False, 'score': score/max_score, 'reasons': reasons}
|
||||
|
||||
# 综合判断(极致放宽条件)
|
||||
condition_met = sum(1 for r in reasons if r.startswith('✓')) >= 2 # 从≥3降低到≥2(极致放宽)
|
||||
score_ratio = score / max_score
|
||||
|
||||
should_add = condition_met and score_ratio >= 0.35 # 从0.5降低到0.35(极致放宽)
|
||||
|
||||
return {
|
||||
'should_add': should_add,
|
||||
'score': score_ratio,
|
||||
'reasons': reasons,
|
||||
'condition_met': condition_met
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{pair}] 加仓条件检查出错: {str(e)}")
|
||||
return {'should_add': False, 'score': 0, 'reasons': [f'错误: {str(e)}']}
|
||||
|
||||
def _calculate_add_position_amount(self, trade: 'Trade', entry_count: int, min_stake: float, max_stake: float) -> float:
|
||||
"""
|
||||
智能计算加仓金额(支持递减策略)
|
||||
- 早期加仓金额较大,后期逐步减小
|
||||
- 防止后期加仓金额过大导致爆仓
|
||||
"""
|
||||
try:
|
||||
initial_stake = float(trade.orders[0].cost)
|
||||
|
||||
# 基础公式:(adjust_multiplier × initial_stake) ^ entry_count
|
||||
base_amount = (self.adjust_multiplier.value * initial_stake) ** entry_count
|
||||
|
||||
# 应用递减系数(后续加仓金额逐步缩小)
|
||||
# 第1次加仓: 100% × 基础金额
|
||||
# 第2次加仓: 75% × 基础金额
|
||||
# 第3次加仓: 56% × 基础金额
|
||||
decrease_ratio = self.add_position_decrease_ratio.value ** (entry_count - 1)
|
||||
adjusted_amount = base_amount * decrease_ratio
|
||||
|
||||
# 安全校验
|
||||
current_stake = float(trade.stake_amount)
|
||||
remaining_capacity = max_stake - current_stake
|
||||
|
||||
# 加仓金额不能超过剩余容量的80%(留余量)
|
||||
adjusted_amount = min(adjusted_amount, remaining_capacity * 0.8)
|
||||
adjusted_amount = max(min_stake, min(adjusted_amount, max_stake - current_stake))
|
||||
|
||||
return adjusted_amount
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{trade.pair}] 加仓金额计算出错: {str(e)}")
|
||||
return 0.0
|
||||
|
||||
def adjust_trade_position(self, trade: 'Trade', current_time, current_rate: float,
|
||||
current_profit: float, min_stake: float, max_stake: float, **kwargs) -> float:
|
||||
"""
|
||||
增强版持仓调整逻辑:加仓精准度 + 递减策略 + 减仓优化
|
||||
"""
|
||||
pair = trade.pair
|
||||
|
||||
# ========================== 分级止盈减仓逻辑(增强版) ==========================
|
||||
if current_profit > 0:
|
||||
reduce_count = len(trade.select_filled_orders(trade.exit_side))
|
||||
if reduce_count >= self.max_reduce_adjustments.value:
|
||||
return 0.0
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
current_kline_time = dataframe.iloc[-1]['date'].strftime('%Y-%m-%d %H:%M:%S')
|
||||
last_reduce_kline = trade.get_custom_data("last_reduce_kline")
|
||||
if last_reduce_kline == current_kline_time:
|
||||
return 0.0
|
||||
|
||||
initial_stake = float(trade.orders[0].cost)
|
||||
current_stake = float(trade.stake_amount)
|
||||
|
||||
# 分级止盈逻辑(3级)
|
||||
# 第1级:达到exit_profit_tier1时,减仓exit_reduce_tier1比例
|
||||
if current_profit >= self.exit_profit_tier1.value:
|
||||
if reduce_count < 1:
|
||||
reduce_amount = current_stake * self.exit_reduce_tier1.value
|
||||
reduce_amount = -min(reduce_amount, current_stake * 0.5) # 单次最多减仓50%
|
||||
|
||||
#logger.info(f"[{pair}] 分级止盈第1级: 盈利{current_profit:.2%}, "
|
||||
# f"减仓比例{self.exit_reduce_tier1.value:.1%}, 金额{abs(reduce_amount):.2f}")
|
||||
trade.set_custom_data("last_reduce_kline", current_kline_time)
|
||||
return max(-current_stake, reduce_amount)
|
||||
|
||||
# 第2级:达到exit_profit_tier2时,减仓exit_reduce_tier2比例
|
||||
if current_profit >= self.exit_profit_tier2.value:
|
||||
if reduce_count < 2:
|
||||
reduce_amount = current_stake * self.exit_reduce_tier2.value
|
||||
reduce_amount = -min(reduce_amount, current_stake * 0.3) # 单次最多减仓30%
|
||||
|
||||
#logger.info(f"[{pair}] 分级止盈第2级: 盈利{current_profit:.2%}, "
|
||||
# f"减仓比例{self.exit_reduce_tier2.value:.1%}, 金额{abs(reduce_amount):.2f}")
|
||||
trade.set_custom_data("last_reduce_kline", current_kline_time)
|
||||
return max(-current_stake, reduce_amount)
|
||||
|
||||
# 基础止盈(保持原有逻辑)
|
||||
if current_profit >= self.reduce_profit_base.value:
|
||||
reduce_amount = (float(self.reduce_coefficient.value) * initial_stake) ** (reduce_count + 1)
|
||||
reduce_amount = min(reduce_amount, current_stake * 0.2) # 单次最多减仓20%
|
||||
reduce_amount = -reduce_amount
|
||||
reduce_amount = max(-current_stake, min(reduce_amount, -float(min_stake)))
|
||||
|
||||
#logger.info(f"[{pair}] 基础止盈: 盈利{current_profit:.2%}, 第{reduce_count+1}次, "
|
||||
# f"金额{abs(reduce_amount):.2f}")
|
||||
trade.set_custom_data("last_reduce_kline", current_kline_time)
|
||||
return reduce_amount
|
||||
|
||||
return 0.0
|
||||
|
||||
# ========================== 增强版加仓逻辑 ==========================
|
||||
entry_count = len(trade.orders)
|
||||
if entry_count > self.max_entry_adjustments.value:
|
||||
return 0.0
|
||||
|
||||
initial_price = trade.open_rate
|
||||
if initial_price == 0:
|
||||
return 0.0
|
||||
|
||||
# 获取数据框
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
if dataframe is None or len(dataframe) < 30:
|
||||
return 0.0
|
||||
|
||||
# 检查加仓条件(多维度评分)
|
||||
condition_check = self._check_add_position_conditions(pair, current_rate, current_profit, entry_count, initial_price, dataframe)
|
||||
|
||||
if not condition_check['should_add']:
|
||||
return 0.0
|
||||
|
||||
# 周期限制:每个timeframe仅加仓一次
|
||||
current_kline_time = dataframe.iloc[-1]['date'].strftime('%Y-%m-%d %H:%M:%S')
|
||||
last_add_kline = trade.get_custom_data("last_add_kline")
|
||||
if last_add_kline == current_kline_time:
|
||||
return 0.0
|
||||
|
||||
# 计算加仓金额
|
||||
additional_stake = self._calculate_add_position_amount(trade, entry_count, min_stake, max_stake)
|
||||
|
||||
if additional_stake > 0:
|
||||
#logger.info(f"[{pair}] 加仓触发: 第{entry_count+1}次, 金额{additional_stake:.2f}, 评分{condition_check['score']:.2f}")
|
||||
trade.set_custom_data("last_add_kline", current_kline_time)
|
||||
return additional_stake
|
||||
|
||||
return 0.0
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time, current_rate: float,
|
||||
current_profit: float, **kwargs) -> float:
|
||||
# 动态止损基于ATR
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1]
|
||||
atr = last_candle['atr']
|
||||
|
||||
# 获取当前市场状态
|
||||
current_state = dataframe['market_state'].iloc[-1] if 'market_state' in dataframe.columns else 'unknown'
|
||||
|
||||
# 渐进式止损策略(盈利越高,止损范围越大)
|
||||
if current_profit > 0.05: # 利润超过5%时
|
||||
return -3.0 * atr / current_rate # 大幅扩大止损范围,让利润奔跑
|
||||
elif current_profit > 0.03: # 利润超过3%时
|
||||
return -2.5 * atr / current_rate # 中等扩大止损范围
|
||||
elif current_profit > 0.01: # 利润超过1%时
|
||||
return -2.0 * atr / current_rate # 轻微扩大止损范围
|
||||
|
||||
# 在强劲牛市中,即使小亏损也可以容忍更大回调
|
||||
if current_state == 'strong_bull' and current_profit > -0.01:
|
||||
return -1.5 * atr / current_rate
|
||||
|
||||
# 基础止损
|
||||
if atr > 0:
|
||||
return -1.2 * atr / current_rate # 基础1.2倍ATR止损
|
||||
return self.stoploss
|
||||
|
||||
279
user_data/strategies/freqprimer.py_origin
Normal file
279
user_data/strategies/freqprimer.py_origin
Normal file
@ -0,0 +1,279 @@
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FreqaiPrimer(IStrategy):
|
||||
minimal_roi = {
|
||||
0: 0.135,
|
||||
9: 0.052,
|
||||
15: 0.007,
|
||||
60: 0
|
||||
}
|
||||
stoploss = -0.263
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.324
|
||||
trailing_stop_positive_offset = 0.411
|
||||
trailing_only_offset_is_reached = False
|
||||
max_open_trades = 4
|
||||
process_only_new_candles = True
|
||||
use_exit_signal = True
|
||||
startup_candle_count: int = 40
|
||||
can_short = False
|
||||
|
||||
buy_rsi = IntParameter(low=10, high=50, default=30, space="buy", optimize=False, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=90, default=70, space="sell", optimize=False, load=True)
|
||||
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.135, space="roi", optimize=True, load=True)
|
||||
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.052, space="roi", optimize=True, load=True)
|
||||
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.007, space="roi", optimize=True, load=True)
|
||||
stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.263, space="stoploss", optimize=True, load=True)
|
||||
trailing_stop_positive_param = DecimalParameter(low=0.1, high=0.5, default=0.324, space="trailing", optimize=True, load=True)
|
||||
trailing_stop_positive_offset_param = DecimalParameter(low=0.2, high=0.6, default=0.411, space="trailing", optimize=True, load=True)
|
||||
|
||||
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,
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 200,
|
||||
"learning_rate": 0.05,
|
||||
"num_leaves": 31,
|
||||
"verbose": -1,
|
||||
},
|
||||
}
|
||||
|
||||
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 feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||||
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=period, stds=2.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"]
|
||||
dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"]
|
||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||
dataframe["%-relative_volume-period"] = (
|
||||
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||||
)
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||||
dataframe = dataframe.ffill()
|
||||
dataframe = dataframe.fillna(0)
|
||||
return dataframe
|
||||
|
||||
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 = dataframe.replace([np.inf, -np.inf], 0)
|
||||
dataframe = dataframe.ffill()
|
||||
dataframe = dataframe.fillna(0)
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
if len(dataframe["close"]) < 20:
|
||||
logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整")
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||||
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"]
|
||||
if "%-volatility" not in dataframe.columns:
|
||||
logger.warning("缺少 %-volatility 列,强制重新生成")
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||||
|
||||
# 移除 shift(-label_period),改为使用当前及过去的数据
|
||||
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
dataframe["&-buy_rsi"] = dataframe["&-buy_rsi"].rolling(window=label_period).mean().ffill().bfill()
|
||||
|
||||
for col in ["&-buy_rsi", "%-volatility"]:
|
||||
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():
|
||||
logger.warning(f"目标列 {col} 仍包含 NaN,数据预览:\n{dataframe[col].tail(10)}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||||
raise
|
||||
|
||||
logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}")
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.info(f"处理交易对:{metadata['pair']}")
|
||||
logger.debug(f"输入特征列:{list(dataframe.columns)}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
logger.debug(f"FreqAI 输出特征列:{list(dataframe.columns)}")
|
||||
|
||||
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)
|
||||
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
# 使用滚动窗口而非未来函数来生成 up_or_down 列
|
||||
dataframe["up_or_down"] = np.where(
|
||||
dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0
|
||||
)
|
||||
|
||||
if "&-buy_rsi" in dataframe.columns:
|
||||
if "%-volatility" not in dataframe.columns:
|
||||
logger.warning("缺少 %-volatility 列,强制重新生成")
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||||
|
||||
dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30
|
||||
dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05)
|
||||
dataframe["&-roi_0"] = (dataframe["close"].rolling(window=label_period).mean() / dataframe["close"] - 1).clip(0, 0.2)
|
||||
|
||||
for col in ["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0"]:
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
|
||||
dataframe[col] = dataframe[col].ffill()
|
||||
dataframe[col] = dataframe[col].fillna(0)
|
||||
|
||||
dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50)
|
||||
dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90)
|
||||
dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1)
|
||||
dataframe["roi_0_pred"] = dataframe["&-roi_0"].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,填充为默认值")
|
||||
dataframe[col] = dataframe[col].ffill()
|
||||
dataframe[col] = dataframe[col].fillna(dataframe[col].mean())
|
||||
|
||||
dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3)
|
||||
dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4)
|
||||
|
||||
self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1])
|
||||
self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1])
|
||||
self.stoploss = float(self.stoploss_param.value)
|
||||
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 = float(self.trailing_stop_positive_param.value)
|
||||
self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value)
|
||||
|
||||
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}")
|
||||
else:
|
||||
logger.warning(f"&-buy_rsi 列缺失,跳过 FreqAI 预测逻辑,检查 freqai.start 输出")
|
||||
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||||
dataframe = dataframe.ffill()
|
||||
dataframe = dataframe.fillna(0)
|
||||
|
||||
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()}")
|
||||
logger.debug(f"最终特征列:{list(dataframe.columns)}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
enter_long_conditions = [
|
||||
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]),
|
||||
df["tema"] > df["tema"].shift(1),
|
||||
df["volume"] > 0,
|
||||
df["do_predict"] == 1,
|
||||
df["up_or_down"] == 1
|
||||
]
|
||||
if enter_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_long_conditions),
|
||||
["enter_long", "enter_tag"]
|
||||
] = (1, "long")
|
||||
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["close"].shift(1) * 0.97),
|
||||
df["volume"] > 0,
|
||||
df["do_predict"] == 1,
|
||||
df["up_or_down"] == 0
|
||||
]
|
||||
if exit_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, exit_long_conditions),
|
||||
"exit_long"
|
||||
] = 1
|
||||
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:
|
||||
try:
|
||||
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
if df is None or df.empty:
|
||||
logger.warning(f"无法获取 {pair} 的分析数据,拒绝交易")
|
||||
return False
|
||||
|
||||
last_candle = df.iloc[-1].squeeze()
|
||||
if "close" not in last_candle or np.isnan(last_candle["close"]):
|
||||
logger.warning(f"{pair} 的最新 K 线缺少有效 close 价格,拒绝交易")
|
||||
return False
|
||||
|
||||
if side == "long":
|
||||
max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值
|
||||
if rate > max_rate:
|
||||
logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}")
|
||||
return False
|
||||
elif side == "short":
|
||||
logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})")
|
||||
return False
|
||||
|
||||
logger.debug(f"确认 {pair} 的交易:side={side}, rate={rate}, close={last_candle['close']}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"确认 {pair} 交易时出错:{str(e)}")
|
||||
return False
|
||||
51
user_data/strategies/market_regime_update_summary.md
Normal file
51
user_data/strategies/market_regime_update_summary.md
Normal file
@ -0,0 +1,51 @@
|
||||
# 风险评级 → 市场状态/波动率制度 替换总结
|
||||
|
||||
## 变更概述
|
||||
已将策略中的"风险评级"维度替换为"市场状态/波动率制度"维度,解决了与趋势得分的同质化问题。
|
||||
|
||||
## 具体变更
|
||||
|
||||
### 1. 特征工程变更
|
||||
- **删除的风险评级特征**:
|
||||
- `%-volatility_ratio` (波动率比率)
|
||||
- `%-price_range` (价格范围)
|
||||
- `%-volume_volatility` (成交量波动率)
|
||||
- `%-price_velocity` (价格动量)
|
||||
- `%-rsi_velocity` (RSI动量)
|
||||
|
||||
- **新增的市场状态特征**:
|
||||
- `%-bb_width` (Bollinger Band宽度)
|
||||
- `%-bb_width_ratio` (BB宽度百分比)
|
||||
- `%-volatility_regime` (波动率制度)
|
||||
- `%-regime_stability` (制度稳定性)
|
||||
- `%-price_efficiency` (价格效率指标)
|
||||
|
||||
### 2. 目标变量变更
|
||||
- **旧目标**:`&*-risk_rating` (0-4级风险评级)
|
||||
- **新目标**:`&*-market_regime` (0-4级市场状态)
|
||||
|
||||
### 3. 市场状态分类定义
|
||||
- **0 - 低波动震荡**:低波动率,价格稳定
|
||||
- **1 - 正常趋势**:适中的波动率和趋势
|
||||
- **2 - 高波动趋势**:高波动但趋势明确
|
||||
- **3 - 极端波动**:极高波动率,市场不稳定
|
||||
- **4 - 黑天鹅状态**:极端市场状况
|
||||
|
||||
### 4. 策略逻辑更新
|
||||
- **仓位调整**:基于市场状态而非风险评级
|
||||
- **入场条件**:使用市场状态调整入场严格程度
|
||||
- **止损设置**:根据市场状态动态调整
|
||||
|
||||
### 5. 配置文件更新
|
||||
- 将`freqai_info["model_training_parameters"]`中的"risk_rating"模型配置改为"market_regime"
|
||||
|
||||
## 优势
|
||||
1. **解决同质化**:市场状态与趋势得分关注不同维度,相关性更低
|
||||
2. **更直观**:直接反映市场波动状态而非抽象风险概念
|
||||
3. **实战价值**:可直接指导仓位管理和风险控制
|
||||
4. **特征丰富**:基于Bollinger Band、波动率持续性等多维度构建
|
||||
|
||||
## 使用建议
|
||||
1. 重新训练模型以适应新的市场状态分类
|
||||
2. 监控市场状态分布,确保各状态均衡
|
||||
3. 根据实际表现微调状态阈值
|
||||
37
user_data/strategies/martingale.json
Normal file
37
user_data/strategies/martingale.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"strategy_name": "MartinGale",
|
||||
"params": {
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 1
|
||||
},
|
||||
"buy": {
|
||||
"add_position_callback": 0.014,
|
||||
"max_entry_adjustments": 14,
|
||||
"rsi_length": 18,
|
||||
"rsi_oversold": 30,
|
||||
"stake_divisor": 0.91,
|
||||
"step_coefficient": 1.03
|
||||
},
|
||||
"sell": {
|
||||
"take_profit_target": 0.027
|
||||
},
|
||||
"protection": {},
|
||||
"roi": {
|
||||
"0": 0.206,
|
||||
"21": 0.055,
|
||||
"56": 0.028,
|
||||
"111": 0
|
||||
},
|
||||
"stoploss": {
|
||||
"stoploss": -0.317
|
||||
},
|
||||
"trailing": {
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.189,
|
||||
"trailing_stop_positive_offset": 0.196,
|
||||
"trailing_only_offset_is_reached": true
|
||||
}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-10-14 04:50:03.146726+00:00"
|
||||
}
|
||||
346
user_data/strategies/martingale.py
Normal file
346
user_data/strategies/martingale.py
Normal file
@ -0,0 +1,346 @@
|
||||
import logging
|
||||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||||
from pandas import DataFrame
|
||||
import pandas_ta as ta
|
||||
from freqtrade.persistence import Trade
|
||||
import numpy as np
|
||||
import datetime
|
||||
import pandas as pd
|
||||
import math
|
||||
|
||||
# 设置pandas选项以解决FutureWarning警告
|
||||
pd.set_option('future.no_silent_downcasting', True)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MartinGale(IStrategy):
|
||||
# 策略参数
|
||||
loglevel = "warning"
|
||||
minimal_roi = {}
|
||||
|
||||
# 启用自定义ROI回调函数
|
||||
use_custom_roi = True
|
||||
|
||||
# 固定止损设置为与OKX App一致
|
||||
stoploss = -0.12 # 对应OKX App中的12.00%止损
|
||||
trailing_stop = True
|
||||
trailing_stop_positive_offset = 0.005 # 跟踪止损偏移量 0.5%
|
||||
|
||||
# 用于跟踪市场状态的数据框缓存
|
||||
_dataframe_cache = None
|
||||
|
||||
def __init__(self, config=None):
|
||||
"""初始化策略参数"""
|
||||
super().__init__(config)
|
||||
self._trailing_stop_positive_default = 0.004 # 降低默认值以更容易触发跟踪止盈
|
||||
|
||||
@property
|
||||
def protections(self):
|
||||
"""
|
||||
保护机制配置
|
||||
基于最新Freqtrade规范,保护机制应定义在策略文件中而非配置文件
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 60, # 3小时回看期(60根3分钟K线)
|
||||
"trade_limit": 2, # 最多2笔止损交易
|
||||
"stop_duration_candles": 60, # 暂停180分钟(60根3分钟K线)
|
||||
"only_per_pair": False # 仅针对单个币对
|
||||
},
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": 2 # 6分钟冷却期(2根3分钟K线)
|
||||
},
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"lookback_period_candles": 48, # 2.4小时回看期
|
||||
"trade_limit": 4, # 4笔交易限制
|
||||
"stop_duration_candles": 24, # 72分钟暂停(24根3分钟K线)
|
||||
"max_allowed_drawdown": 0.20 # 20%最大回撤容忍度
|
||||
}
|
||||
]
|
||||
|
||||
@property
|
||||
def trailing_stop_positive(self):
|
||||
"""根据市场状态动态调整跟踪止盈参数"""
|
||||
# 获取当前市场状态
|
||||
if self._dataframe_cache is not None and len(self._dataframe_cache) > 0:
|
||||
current_state = self._dataframe_cache['market_state'].iloc[-1]
|
||||
if current_state == 'strong_bull':
|
||||
return 0.007 # 强劲牛市中降低跟踪止盈,让利润奔跑
|
||||
elif current_state == 'weak_bull':
|
||||
return 0.005 # 弱势牛市中保持较低的跟踪止盈
|
||||
return self._trailing_stop_positive_default # 返回默认值
|
||||
|
||||
@trailing_stop_positive.setter
|
||||
def trailing_stop_positive(self, value):
|
||||
"""设置trailing_stop_positive的默认值"""
|
||||
self._trailing_stop_positive_default = value
|
||||
|
||||
# 时间框架设置 - 主时间框架为3分钟,与OKX App一致
|
||||
timeframe = "3m" # 主时间框架为 3 分钟
|
||||
can_short = False # 禁用做空
|
||||
|
||||
# 自定义指标参数 - 设置为可通过Hyperopt优化
|
||||
# 从OKX App中获取的参数
|
||||
rsi_length = IntParameter(7, 21, default=14, optimize=True, load=True, space='buy') # RSI周期为14,可优化范围7-21
|
||||
rsi_oversold = IntParameter(20, 40, default=30, optimize=True, load=True, space='buy') # RSI触发阈值为30,可优化范围20-40
|
||||
|
||||
# 马丁格尔策略参数 - 基于OKX App界面设置
|
||||
max_entry_adjustments = IntParameter(5, 15, default=10, optimize=True, load=True, space='buy') # 最大加仓次数10次,可优化范围5-15
|
||||
add_position_callback = DecimalParameter(0.005, 0.015, decimals=3, default=0.0066, optimize=True, load=True, space='buy') # 跌幅加仓阈值0.66%,可优化范围0.5%-1.5%
|
||||
|
||||
# 马丁格尔加仓比例参数
|
||||
step_coefficient = DecimalParameter(1.0, 1.1, decimals=2, default=1.05, optimize=True, load=True, space='buy') # 加仓比例1.05倍,可优化范围1.0-1.1
|
||||
stake_divisor = DecimalParameter(0.9, 1.1, decimals=2, default=1.0, optimize=True, load=True, space='buy') # 初始比例,可优化范围0.9-1.1
|
||||
|
||||
# 止盈目标参数 - 对应OKX App中的单周期止盈目标2.50%
|
||||
take_profit_target = DecimalParameter(0.02, 0.03, decimals=3, default=0.025, optimize=True, load=True, space='sell') # 止盈目标2.5%,可优化范围2.0%-3.0%
|
||||
|
||||
def informative_pairs(self):
|
||||
"""定义辅助时间框架"""
|
||||
pairs = self.dp.current_whitelist()
|
||||
# 添加15m和1h作为辅助时间框架
|
||||
return [(pair, '15m') for pair in pairs] + [(pair, '1h') for pair in pairs]
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: pd.Timestamp,
|
||||
current_rate: float,
|
||||
proposed_stake: float,
|
||||
min_stake: float,
|
||||
max_stake: float,
|
||||
**kwargs) -> float:
|
||||
"""自定义下单金额"""
|
||||
# 初始资金设置
|
||||
initial_balance = self.config.get('dry_run_wallet', 10000)
|
||||
# 固定使用初始资金的5%作为初次下单金额(在OKX App范围内)
|
||||
desired_stake = initial_balance * 0.05
|
||||
desired_stake = math.floor(desired_stake) # 取整
|
||||
# 确保在OKX App设定的范围内(2-50,000 USDT)
|
||||
return max(min(desired_stake, 50000, max_stake), 2, min_stake)
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""计算各种技术指标"""
|
||||
# 计算3m周期的指标
|
||||
rsi_length_value = self.rsi_length.value
|
||||
|
||||
# 计算3m RSI指标
|
||||
dataframe['rsi_3m'] = ta.rsi(dataframe['close'], length=rsi_length_value)
|
||||
|
||||
# 计算3m时间框架的EMA50和EMA200
|
||||
dataframe['ema_50_3m'] = ta.ema(dataframe['close'], length=50)
|
||||
dataframe['ema_200_3m'] = ta.ema(dataframe['close'], length=200)
|
||||
|
||||
# 成交量过滤
|
||||
dataframe['volume_ma'] = dataframe['volume'].rolling(20).mean()
|
||||
|
||||
# 计算ATR用于动态止损
|
||||
dataframe['atr'] = ta.atr(dataframe['high'], dataframe['low'], dataframe['close'], length=14)
|
||||
|
||||
# 获取15m数据
|
||||
df_15m = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='15m')
|
||||
df_15m['rsi_15m'] = ta.rsi(df_15m['close'], length=rsi_length_value)
|
||||
# 计算15m时间框架的EMA50和EMA200
|
||||
df_15m['ema_50_15m'] = ta.ema(df_15m['close'], length=50)
|
||||
df_15m['ema_200_15m'] = ta.ema(df_15m['close'], length=200)
|
||||
|
||||
# 将15m数据重新索引到主时间框架(3m)
|
||||
df_15m = df_15m.set_index('date').reindex(dataframe['date']).reset_index()
|
||||
df_15m = df_15m.rename(columns={'index': 'date'})
|
||||
df_15m = df_15m[['date', 'rsi_15m', 'ema_50_15m', 'ema_200_15m']].ffill()
|
||||
|
||||
# 合并15m数据
|
||||
dataframe = dataframe.merge(df_15m, how='left', on='date')
|
||||
|
||||
# 获取1h数据
|
||||
df_1h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1h')
|
||||
|
||||
# 计算1h RSI和EMA
|
||||
df_1h['rsi_1h'] = ta.rsi(df_1h['close'], length=rsi_length_value)
|
||||
df_1h['ema_50_1h'] = ta.ema(df_1h['close'], length=50) # 1h 50周期EMA
|
||||
df_1h['ema_200_1h'] = ta.ema(df_1h['close'], length=200) # 1h 200周期EMA
|
||||
|
||||
# 将1h数据重新索引到主时间框架(3m),并填充缺失值
|
||||
df_1h = df_1h.set_index('date').reindex(dataframe['date']).ffill().bfill().reset_index()
|
||||
|
||||
# 计算1h上涨趋势,确保不包含None值
|
||||
df_1h['trend_1h'] = df_1h['close'] > df_1h['ema_50_1h'] # 1h上涨趋势
|
||||
df_1h = df_1h.rename(columns={'index': 'date'})
|
||||
df_1h = df_1h[['date', 'rsi_1h', 'trend_1h', 'ema_50_1h', 'ema_200_1h']].ffill()
|
||||
|
||||
# 合并1h数据
|
||||
dataframe = dataframe.merge(df_1h, how='left', on='date').ffill()
|
||||
|
||||
# 计算各时间框架的趋势状态(牛/熊)
|
||||
# 3m时间框架:ema50下穿ema200为熊,上穿为牛
|
||||
dataframe['trend_3m'] = np.where(dataframe['ema_50_3m'] > dataframe['ema_200_3m'], 1, 0)
|
||||
|
||||
# 15m时间框架:ema50下穿ema200为熊,上穿为牛
|
||||
dataframe['trend_15m'] = np.where(dataframe['ema_50_15m'] > dataframe['ema_200_15m'], 1, 0)
|
||||
|
||||
# 1h时间框架:ema50下穿ema200为熊,上穿为牛
|
||||
dataframe['trend_1h_ema'] = np.where(dataframe['ema_50_1h'] > dataframe['ema_200_1h'], 1, 0)
|
||||
|
||||
# 计算熊牛得分(0-100)
|
||||
# 权重:3m熊牛权重10,15m熊牛权重35,1h熊牛权重65
|
||||
dataframe['market_score'] = (
|
||||
dataframe['trend_3m'] * 10 +
|
||||
dataframe['trend_15m'] * 35 +
|
||||
dataframe['trend_1h_ema'] * 65
|
||||
)
|
||||
|
||||
# 确保得分在0-100范围内
|
||||
dataframe['market_score'] = dataframe['market_score'].clip(lower=0, upper=100)
|
||||
|
||||
# 根据得分分类市场状态
|
||||
dataframe['market_state'] = 'neutral'
|
||||
dataframe.loc[dataframe['market_score'] > 70, 'market_state'] = 'strong_bull'
|
||||
dataframe.loc[(dataframe['market_score'] > 50) & (dataframe['market_score'] <= 70), 'market_state'] = 'weak_bull'
|
||||
dataframe.loc[(dataframe['market_score'] >= 30) & (dataframe['market_score'] <= 50), 'market_state'] = 'neutral'
|
||||
dataframe.loc[(dataframe['market_score'] > 10) & (dataframe['market_score'] < 30), 'market_state'] = 'weak_bear'
|
||||
dataframe.loc[dataframe['market_score'] <= 10, 'market_state'] = 'strong_bear'
|
||||
|
||||
# 创建一个使用前一行市场状态的列
|
||||
dataframe['prev_market_state'] = dataframe['market_state'].shift(1)
|
||||
# 为第一行设置默认值
|
||||
dataframe['prev_market_state'] = dataframe['prev_market_state'].fillna('neutral')
|
||||
|
||||
# 保存数据框缓存用于trailing_stop_positive计算
|
||||
self._dataframe_cache = dataframe
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""设置出场条件"""
|
||||
# 基于固定止盈目标(2.50%)
|
||||
# 注意:实际止盈将在custom_exit中实现
|
||||
|
||||
# 当RSI进入超买区域时考虑出场
|
||||
rsi_overbought = dataframe['rsi_1h'] > 70
|
||||
|
||||
# 设置出场信号
|
||||
dataframe.loc[rsi_overbought, 'exit_long'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""设置入场条件"""
|
||||
# 确保prev_market_state列存在
|
||||
if 'prev_market_state' not in dataframe.columns:
|
||||
dataframe['prev_market_state'] = 'neutral'
|
||||
|
||||
# 基于OKX App的RSI条件:RSI-14向下穿过30阈值
|
||||
# 条件1: RSI处于超卖区域(根据市场状态动态调整)
|
||||
rsi_condition = dataframe.apply(lambda row:
|
||||
row['rsi_3m'] < self.rsi_oversold.value if row['prev_market_state'] in ['strong_bull', 'weak_bull'] else row['rsi_3m'] < self.rsi_oversold.value,
|
||||
axis=1)
|
||||
|
||||
# 条件2: RSI向下穿过阈值(交叉检测)
|
||||
rsi_cross_down = (dataframe['rsi_3m'] < self.rsi_oversold.value) & (dataframe['rsi_3m'].shift(1) >= self.rsi_oversold.value)
|
||||
|
||||
# 条件3: 成交量放大确认信号
|
||||
volume_spike = dataframe['volume'] > dataframe['volume_ma'] * 1.2
|
||||
|
||||
# 条件4: 至少有一个时间框架的趋势确认
|
||||
trend_confirmation = (dataframe['trend_3m'] == 1) | (dataframe['trend_15m'] == 1) | (dataframe['trend_1h_ema'] == 1)
|
||||
|
||||
# 合并所有条件
|
||||
final_condition = rsi_cross_down & volume_spike & trend_confirmation
|
||||
|
||||
# 设置入场信号
|
||||
dataframe.loc[final_condition, 'enter_long'] = 1
|
||||
|
||||
# 日志记录
|
||||
# if dataframe['enter_long'].sum() > 0:
|
||||
# logger.info(f"[{metadata['pair']}] 发现入场信号数量: {dataframe['enter_long'].sum()}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time, current_rate: float,
|
||||
current_profit: float, **kwargs) -> float:
|
||||
"""自定义动态止损"""
|
||||
# 动态止损基于ATR
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1]
|
||||
atr = last_candle['atr']
|
||||
|
||||
# 获取当前市场状态
|
||||
current_state = dataframe['market_state'].iloc[-1] if 'market_state' in dataframe.columns else 'unknown'
|
||||
|
||||
# 更激进的渐进式止损策略
|
||||
if current_profit > 0.05: # 利润超过5%时
|
||||
return -3.0 * atr / current_rate # 更大幅扩大止损范围,让利润奔跑
|
||||
elif current_profit > 0.03: # 利润超过3%时
|
||||
return -2.5 * atr / current_rate # 更中等扩大止损范围
|
||||
elif current_profit > 0.01: # 利润超过1%时
|
||||
return -2.0 * atr / current_rate # 更轻微扩大止损范围
|
||||
|
||||
# 在强劲牛市中,即使小亏损也可以容忍更大回调
|
||||
if current_state == 'strong_bull' and current_profit > -0.01:
|
||||
return -1.8 * atr / current_rate
|
||||
|
||||
if atr > 0:
|
||||
return -1.2 * atr / current_rate # 基础1.2倍ATR止损
|
||||
return self.stoploss
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||
current_profit: float, **kwargs) -> float:
|
||||
"""自定义出场逻辑,实现固定止盈目标"""
|
||||
if trade.is_short:
|
||||
return 0.0
|
||||
|
||||
# 基于OKX App的单周期止盈目标2.50%
|
||||
if current_profit >= self.take_profit_target.value:
|
||||
return 1.0 # 全额出场
|
||||
|
||||
# 未达到止盈目标,不出场
|
||||
return 0.0
|
||||
|
||||
def adjust_trade_position(self, trade: 'Trade', current_time, current_rate: float,
|
||||
current_profit: float, min_stake: float, max_stake: float, **kwargs) -> float:
|
||||
"""
|
||||
实现马丁格尔加仓逻辑
|
||||
- 基于OKX App的参数:跌幅0.66%加仓,最大10次加仓,1.05倍加仓比例
|
||||
"""
|
||||
# 获取当前交易对
|
||||
pair = trade.pair
|
||||
|
||||
# 获取当前交易的加仓次数
|
||||
entry_count = len(trade.orders) # 获取所有入场订单数量
|
||||
|
||||
# 如果已经达到最大加仓次数,则不再加仓
|
||||
if entry_count - 1 >= self.max_entry_adjustments.value:
|
||||
logger.info(f"[{pair}] 已达到最大加仓次数 {self.max_entry_adjustments.value},停止加仓")
|
||||
return 0.0
|
||||
|
||||
# 获取初始入场价格和当前价格的差值百分比
|
||||
initial_price = trade.open_rate
|
||||
if initial_price == 0:
|
||||
return 0.0
|
||||
price_diff_pct = (current_rate - initial_price) / initial_price
|
||||
|
||||
# 检查价格回调是否达到加仓间隔(OKX App中的0.66%跌幅)
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
current_state = dataframe['market_state'].iloc[-1] if 'market_state' in dataframe.columns else 'neutral'
|
||||
|
||||
if price_diff_pct <= -self.add_position_callback.value and current_state not in ['bear', 'weak_bear']:
|
||||
# 计算初始入场金额
|
||||
initial_stake = trade.orders[0].cost # 第一笔订单的成本
|
||||
|
||||
# 计算加仓次数(从1开始计数)
|
||||
adjustment_count = entry_count - 1 # 已加仓次数
|
||||
|
||||
# 计算加仓金额: (initial_stake * step_coefficient) ^ (adjustment_count + 1) / stake_divisor
|
||||
# 实现OKX App中的1.05倍加仓比例
|
||||
additional_stake = (self.step_coefficient.value * initial_stake / self.stake_divisor.value) ** (adjustment_count + 1)
|
||||
|
||||
# 确保加仓金额在允许的范围内(≥2 USDT,与OKX App一致)
|
||||
additional_stake = max(min_stake, min(additional_stake, max_stake - trade.stake_amount))
|
||||
|
||||
logger.info(f"[{pair}] 触发加仓: 第{adjustment_count + 1}次加仓, 初始金额{initial_stake:.2f}, \
|
||||
加仓金额{additional_stake:.2f}, 价格差{price_diff_pct:.2%}, 当前利润{current_profit:.2%}")
|
||||
|
||||
return additional_stake
|
||||
|
||||
# 不符合加仓条件,返回0
|
||||
return 0.0
|
||||
49
user_data/strategies/mystrategy.json
Normal file
49
user_data/strategies/mystrategy.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"strategy_name": "MyStrategy",
|
||||
"params": {
|
||||
"roi": {},
|
||||
"stoploss": {
|
||||
"stoploss": -0.14
|
||||
},
|
||||
"trailing": {
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.0125,
|
||||
"trailing_stop_positive_offset": 0.045,
|
||||
"trailing_only_offset_is_reached": false
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
},
|
||||
"buy": {
|
||||
"bb_std": 3.0,
|
||||
"bb_width_threshold": 0.012,
|
||||
"h1_max_candles": 200,
|
||||
"h1_max_consecutive_candles": 3,
|
||||
"max_entry_adjustments": 4,
|
||||
"rsi_bull_threshold": 54,
|
||||
"rsi_length": 16,
|
||||
"stochrsi_bull_threshold": 36,
|
||||
"volume_multiplier": 1.6,
|
||||
"add_position_callback": 0.053,
|
||||
"bb_length": 14,
|
||||
"bb_lower_deviation": 1.05,
|
||||
"h1_rapid_rise_threshold": 0.065,
|
||||
"min_condition_count": 2,
|
||||
"rsi_oversold": 42,
|
||||
"stake_divisor": 2.793,
|
||||
"step_coefficient": 1.45,
|
||||
"stochrsi_neutral_threshold": 29
|
||||
},
|
||||
"sell": {
|
||||
"exit_bb_upper_deviation": 0.99,
|
||||
"exit_volume_multiplier": 1.7,
|
||||
"roi_param_a": -6e-05,
|
||||
"roi_param_k": 132,
|
||||
"roi_param_t": 0.168,
|
||||
"rsi_overbought": 58
|
||||
},
|
||||
"protection": {}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-10-05 16:38:39.948030+00:00"
|
||||
}
|
||||
279
user_data/strategies/mystrategy.py
Normal file
279
user_data/strategies/mystrategy.py
Normal file
@ -0,0 +1,279 @@
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MyStrategy(IStrategy):
|
||||
minimal_roi = {
|
||||
0: 0.135,
|
||||
9: 0.052,
|
||||
15: 0.007,
|
||||
60: 0
|
||||
}
|
||||
stoploss = -0.263
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.324
|
||||
trailing_stop_positive_offset = 0.411
|
||||
trailing_only_offset_is_reached = False
|
||||
max_open_trades = 4
|
||||
process_only_new_candles = True
|
||||
use_exit_signal = True
|
||||
startup_candle_count: int = 40
|
||||
can_short = False
|
||||
|
||||
buy_rsi = IntParameter(low=10, high=50, default=30, space="buy", optimize=False, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=90, default=70, space="sell", optimize=False, load=True)
|
||||
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.135, space="roi", optimize=True, load=True)
|
||||
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.052, space="roi", optimize=True, load=True)
|
||||
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.007, space="roi", optimize=True, load=True)
|
||||
stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.263, space="stoploss", optimize=True, load=True)
|
||||
trailing_stop_positive_param = DecimalParameter(low=0.1, high=0.5, default=0.324, space="trailing", optimize=True, load=True)
|
||||
trailing_stop_positive_offset_param = DecimalParameter(low=0.2, high=0.6, default=0.411, space="trailing", optimize=True, load=True)
|
||||
|
||||
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,
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 200,
|
||||
"learning_rate": 0.05,
|
||||
"num_leaves": 31,
|
||||
"verbose": -1,
|
||||
},
|
||||
}
|
||||
|
||||
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 feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||||
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=period, stds=2.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"]
|
||||
dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"]
|
||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||
dataframe["%-relative_volume-period"] = (
|
||||
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||||
)
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||||
dataframe = dataframe.ffill()
|
||||
dataframe = dataframe.fillna(0)
|
||||
return dataframe
|
||||
|
||||
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 = dataframe.replace([np.inf, -np.inf], 0)
|
||||
dataframe = dataframe.ffill()
|
||||
dataframe = dataframe.fillna(0)
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
if len(dataframe["close"]) < 20:
|
||||
logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整")
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||||
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"]
|
||||
if "%-volatility" not in dataframe.columns:
|
||||
logger.warning("缺少 %-volatility 列,强制重新生成")
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||||
|
||||
# 移除 shift(-label_period),改为使用当前及过去的数据
|
||||
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
dataframe["&-buy_rsi"] = dataframe["&-buy_rsi"].rolling(window=label_period).mean().ffill().bfill()
|
||||
|
||||
for col in ["&-buy_rsi", "%-volatility"]:
|
||||
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():
|
||||
logger.warning(f"目标列 {col} 仍包含 NaN,数据预览:\n{dataframe[col].tail(10)}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||||
raise
|
||||
|
||||
logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}")
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.info(f"处理交易对:{metadata['pair']}")
|
||||
logger.debug(f"输入特征列:{list(dataframe.columns)}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
logger.debug(f"FreqAI 输出特征列:{list(dataframe.columns)}")
|
||||
|
||||
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)
|
||||
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
# 使用滚动窗口而非未来函数来生成 up_or_down 列
|
||||
dataframe["up_or_down"] = np.where(
|
||||
dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0
|
||||
)
|
||||
|
||||
if "&-buy_rsi" in dataframe.columns:
|
||||
if "%-volatility" not in dataframe.columns:
|
||||
logger.warning("缺少 %-volatility 列,强制重新生成")
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||||
|
||||
dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30
|
||||
dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05)
|
||||
dataframe["&-roi_0"] = (dataframe["close"].rolling(window=label_period).mean() / dataframe["close"] - 1).clip(0, 0.2)
|
||||
|
||||
for col in ["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0"]:
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
|
||||
dataframe[col] = dataframe[col].ffill()
|
||||
dataframe[col] = dataframe[col].fillna(0)
|
||||
|
||||
dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50)
|
||||
dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90)
|
||||
dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1)
|
||||
dataframe["roi_0_pred"] = dataframe["&-roi_0"].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,填充为默认值")
|
||||
dataframe[col] = dataframe[col].ffill()
|
||||
dataframe[col] = dataframe[col].fillna(dataframe[col].mean())
|
||||
|
||||
dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3)
|
||||
dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4)
|
||||
|
||||
self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1])
|
||||
self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1])
|
||||
self.stoploss = float(self.stoploss_param.value)
|
||||
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 = float(self.trailing_stop_positive_param.value)
|
||||
self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value)
|
||||
|
||||
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}")
|
||||
else:
|
||||
logger.warning(f"&-buy_rsi 列缺失,跳过 FreqAI 预测逻辑,检查 freqai.start 输出")
|
||||
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||||
dataframe = dataframe.ffill()
|
||||
dataframe = dataframe.fillna(0)
|
||||
|
||||
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()}")
|
||||
logger.debug(f"最终特征列:{list(dataframe.columns)}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
enter_long_conditions = [
|
||||
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]),
|
||||
df["tema"] > df["tema"].shift(1),
|
||||
df["volume"] > 0,
|
||||
df["do_predict"] == 1,
|
||||
df["up_or_down"] == 1
|
||||
]
|
||||
if enter_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_long_conditions),
|
||||
["enter_long", "enter_tag"]
|
||||
] = (1, "long")
|
||||
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["close"].shift(1) * 0.97),
|
||||
df["volume"] > 0,
|
||||
df["do_predict"] == 1,
|
||||
df["up_or_down"] == 0
|
||||
]
|
||||
if exit_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, exit_long_conditions),
|
||||
"exit_long"
|
||||
] = 1
|
||||
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:
|
||||
try:
|
||||
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
if df is None or df.empty:
|
||||
logger.warning(f"无法获取 {pair} 的分析数据,拒绝交易")
|
||||
return False
|
||||
|
||||
last_candle = df.iloc[-1].squeeze()
|
||||
if "close" not in last_candle or np.isnan(last_candle["close"]):
|
||||
logger.warning(f"{pair} 的最新 K 线缺少有效 close 价格,拒绝交易")
|
||||
return False
|
||||
|
||||
if side == "long":
|
||||
max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值
|
||||
if rate > max_rate:
|
||||
logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}")
|
||||
return False
|
||||
elif side == "short":
|
||||
logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})")
|
||||
return False
|
||||
|
||||
logger.debug(f"确认 {pair} 的交易:side={side}, rate={rate}, close={last_candle['close']}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"确认 {pair} 交易时出错:{str(e)}")
|
||||
return False
|
||||
36
user_data/strategies/old.json
Normal file
36
user_data/strategies/old.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"strategy_name": "FreqaiPrimer",
|
||||
"params": {
|
||||
"roi": {
|
||||
"0": 0.06,
|
||||
"30": 0.04,
|
||||
"90": 0.025,
|
||||
"270": 0.002
|
||||
},
|
||||
"stoploss": {
|
||||
"stoploss": -0.14
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
},
|
||||
"buy": {
|
||||
"ADD_POSITION_THRESHOLD": -0.045,
|
||||
"COOLDOWN_PERIOD_MINUTES": 8,
|
||||
"MAX_ENTRY_POSITION_ADJUSTMENT": 3
|
||||
},
|
||||
"sell": {
|
||||
"EXIT_POSITION_RATIO": 0.665,
|
||||
"TRAILING_STOP_DISTANCE": 0.006,
|
||||
"TRAILING_STOP_START": 0.038
|
||||
},
|
||||
"protection": {},
|
||||
"trailing": {
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.073,
|
||||
"trailing_stop_positive_offset": 0.11099999999999999,
|
||||
"trailing_only_offset_is_reached": false
|
||||
}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-08-02 21:25:00.515292+00:00"
|
||||
}
|
||||
3
user_data/strategies/smartbbgrid.json
Normal file
3
user_data/strategies/smartbbgrid.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"strategy_name": "SmartBBGrid"
|
||||
}
|
||||
19
user_data/strategies/smartbbgrid.py
Normal file
19
user_data/strategies/smartbbgrid.py
Normal file
@ -0,0 +1,19 @@
|
||||
from freqtrade.strategy import IStrategy
|
||||
from pandas import DataFrame
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
import numpy as np
|
||||
|
||||
class SmartBBGrid(IStrategy):
|
||||
INTERFACE_VERSION = 3
|
||||
timeframe = '4h'
|
||||
can_short = False
|
||||
minimal_roi = {"0": 100}
|
||||
stoploss = -0.99
|
||||
startup_candle_count = 250
|
||||
use_exit_signal = True
|
||||
exit_profit_only = False
|
||||
ignore_roi_if_entry_signal = True
|
||||
position_adjustment_enable = True
|
||||
max_entry_position_adjustment = 1
|
||||
|
||||
36
user_data/strategies/staticgrid.py
Normal file
36
user_data/strategies/staticgrid.py
Normal file
@ -0,0 +1,36 @@
|
||||
# /freqtrade/user_data/strategies/StaticGrid.py
|
||||
from freqtrade.strategy import IStrategy
|
||||
from pandas import DataFrame
|
||||
|
||||
class StaticGrid(IStrategy):
|
||||
INTERFACE_VERSION = 3
|
||||
timeframe = '1h'
|
||||
can_short = False
|
||||
minimal_roi = {"0": 100}
|
||||
stoploss = -0.99
|
||||
use_exit_signal = True
|
||||
position_adjustment_enable = True
|
||||
max_entry_position_adjustment = -1
|
||||
|
||||
# 永续网格参数
|
||||
LOWER = 1500.0
|
||||
UPPER = 4500.0
|
||||
STEP = 50.0
|
||||
STAKE = 40.0
|
||||
|
||||
# 必须加这一行!否则报抽象类错误
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
for price in [self.LOWER + i * self.STEP for i in range(int((self.UPPER - self.LOWER) // self.STEP) + 3)]:
|
||||
dataframe.loc[dataframe['low'] <= price, 'enter_long'] = True
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
for price in [self.LOWER + i * self.STEP for i in range(int((self.UPPER - self.LOWER) // self.STEP) + 3)]:
|
||||
dataframe.loc[dataframe['high'] >= price, 'exit_long'] = True
|
||||
return dataframe
|
||||
|
||||
def custom_stake_amount(self, **kwargs) -> float:
|
||||
return self.STAKE
|
||||
Loading…
x
Reference in New Issue
Block a user