963 lines
51 KiB
Python
963 lines
51 KiB
Python
import logging
|
||
import numpy as np
|
||
import datetime
|
||
import datetime
|
||
import os
|
||
import json
|
||
import glob
|
||
from functools import reduce
|
||
from freqtrade.persistence import Trade
|
||
import talib.abstract as ta
|
||
from pandas import DataFrame
|
||
import pandas as pd
|
||
from typing import Dict
|
||
from freqtrade.strategy import (DecimalParameter, IStrategy, IntParameter)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class FreqaiPrimer(IStrategy):
|
||
"""
|
||
基于 FreqAI 的动态阈值交易策略,集成动态加仓、减仓和自定义 ROI 逻辑,兼容最新 Freqtrade 版本
|
||
"""
|
||
|
||
# --- 🧪 Hyperopt Parameters ---
|
||
TRAILING_STOP_START = DecimalParameter(0.01, 0.05, default=0.03, space='sell', optimize=True)
|
||
TRAILING_STOP_DISTANCE = DecimalParameter(0.005, 0.02, default=0.01, space='sell', 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)
|
||
|
||
# 新增:加仓和减仓参数
|
||
ADD_POSITION_THRESHOLD = DecimalParameter(-0.05, -0.01, default=-0.02, space='buy', optimize=True)
|
||
EXIT_POSITION_RATIO = DecimalParameter(0.2, 0.7, default=0.5, space='sell', optimize=True)
|
||
COOLDOWN_PERIOD_MINUTES = IntParameter(1, 10, default=5, space='buy', optimize=True)
|
||
MAX_ENTRY_POSITION_ADJUSTMENT = IntParameter(1, 3, default=2, space='buy', optimize=True)
|
||
|
||
# --- 🛠️ 固定配置参数 ---
|
||
stoploss = -0.15
|
||
timeframe = "3m"
|
||
use_custom_stoploss = True
|
||
position_adjustment_enable = True # 启用动态仓位调整
|
||
|
||
minimal_roi = {
|
||
"0": 0.06, # 30分钟(0-30分钟)内,8% 盈利退出
|
||
"30": 0.04, # 2小时(30-120分钟)内,4% 盈利退出
|
||
"90": 0.025, # 4小时(120-240分钟)内,2% 盈利退出
|
||
"270": 0.002 # 8小时(240-480分钟)内,0% 盈利退出
|
||
}
|
||
|
||
plot_config = {
|
||
"main_plot": {
|
||
"ema200": {"color": "blue"},
|
||
"bb_upperband": {"color": "gray"},
|
||
"bb_lowerband": {"color": "gray"},
|
||
},
|
||
"subplots": {
|
||
"Signals": {
|
||
"enter_long": {"color": "green", "type": "scatter"},
|
||
"exit_long": {"color": "red", "type": "scatter"}
|
||
},
|
||
"Indicators": {
|
||
"&-price_value_divergence": {"color": "purple"},
|
||
"volume_z_score": {"color": "orange"},
|
||
"rsi": {"color": "cyan"},
|
||
"stochrsi_k": {"color": "magenta"},
|
||
},
|
||
"Bearish Signals": {
|
||
"bearish_signal": {"type": "bar", "color": "red"},
|
||
"stochrsi_overbought": {"type": "bar", "color": "orange"},
|
||
}
|
||
}
|
||
}
|
||
|
||
freqai_info = {
|
||
"identifier": "test58",
|
||
"model": "LightGBMRegressor",
|
||
"feature_parameters": {
|
||
"include_timeframes": ["3m", "15m", "1h"],
|
||
"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,
|
||
},
|
||
"fit_live_predictions_candles": 100,
|
||
"live_retrain_candles": 100,
|
||
}
|
||
|
||
@staticmethod
|
||
def linear_map(value, from_min, from_max, to_min, to_max):
|
||
return (value - from_min) / (from_max - from_min) * (to_max - to_min) + to_min
|
||
|
||
def __init__(self, config: dict, *args, **kwargs):
|
||
super().__init__(config, *args, **kwargs)
|
||
# 配置日志格式,包含时间戳
|
||
logging.basicConfig(
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||
level=logging.info,
|
||
datefmt='%Y-%m-%d %H:%M:%S,%f'[:-3] # 毫秒精度
|
||
)
|
||
logger.info("✅ 策略已初始化,日志级别设置为 DEBUG")
|
||
self.trailing_stop_enabled = False
|
||
self.pair_stats = {}
|
||
self.stats_logged = False
|
||
self.fit_live_predictions_candles = self.freqai_info.get("fit_live_predictions_candles", 100)
|
||
self.last_entry_time = {} # 记录每个币种的最后入场时间
|
||
|
||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
|
||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||
|
||
real = ta.TYPPRICE(dataframe)
|
||
upperband, middleband, lowerband = ta.BBANDS(real, timeperiod=period, nbdevup=2.0, nbdevdn=2.0)
|
||
dataframe["bb_lowerband-period"] = lowerband
|
||
dataframe["bb_upperband-period"] = upperband
|
||
dataframe["bb_middleband-period"] = middleband
|
||
dataframe["%-bb_width-period"] = (dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]) / dataframe["bb_middleband-period"]
|
||
|
||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||
dataframe["%-relative_volume-period"] = dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||
|
||
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
|
||
dataframe["%-price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
|
||
|
||
columns_to_clean = [
|
||
"%-rsi-period", "%-mfi-period", "%-sma-period", "%-ema-period", "%-adx-period",
|
||
"bb_lowerband-period", "bb_middleband-period", "bb_upperband-period",
|
||
"%-bb_width-period", "%-relative_volume-period", "%-price_value_divergence"
|
||
]
|
||
for col in columns_to_clean:
|
||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||
|
||
pair = metadata.get('pair', 'Unknown')
|
||
logger.info(f"[{pair}] 特征工程完成,列:{list(dataframe.columns)}")
|
||
return dataframe
|
||
|
||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||
pair = metadata.get('pair', 'Unknown')
|
||
if len(dataframe) < 200:
|
||
logger.warning(f"[{pair}] 数据量不足({len(dataframe)}根K线),需要至少200根K线进行训练")
|
||
return dataframe
|
||
|
||
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
|
||
dataframe["&-price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
|
||
|
||
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"]
|
||
dataframe["volume_z_score"] = dataframe["volume_z_score"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||
|
||
dataframe["&-price_value_divergence"] = dataframe["&-price_value_divergence"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||
|
||
return dataframe
|
||
def is_stochrsi_overbought(self, dataframe: DataFrame, period=10, threshold=85) -> bool:
|
||
"""
|
||
判断当前 STOCHRSI 是否在过去 N 根 K 线中平均高于阈值(如 85)
|
||
"""
|
||
if 'stochrsi_k' not in dataframe.columns:
|
||
# 如果列不存在,返回全 False 的 Series
|
||
return pd.Series([False] * len(dataframe), index=dataframe.index)
|
||
# 计算滚动平均值并判断是否超过阈值
|
||
avg_stochrsi = dataframe['stochrsi_k'].rolling(window=period).mean()
|
||
return avg_stochrsi > threshold
|
||
|
||
def is_bearish_market(self, dataframe: DataFrame, metadata: dict, timeframe: str = "1h") -> pd.Series:
|
||
pair = metadata.get('pair', 'Unknown')
|
||
logger.info(f"[{pair}] 开始计算 1h 熊市信号")
|
||
|
||
# 防御性检查
|
||
required_columns = ['close_1h', 'high_1h', 'low_1h', 'open_1h', 'stochrsi_k_1h', 'stochrsi_d_1h', 'bb_middle_1h']
|
||
missing = [col for col in required_columns if col not in dataframe.columns]
|
||
if missing:
|
||
logger.error(f"[{pair}] 缺少必要列:{missing},返回全 False")
|
||
return pd.Series([False] * len(dataframe), index=dataframe.index)
|
||
|
||
# 检查 volume_z_score_1h 是否存在
|
||
has_volume_z_score = 'volume_z_score_1h' in dataframe.columns and not dataframe['volume_z_score_1h'].isna().all()
|
||
|
||
# 条件 a:价格远低于布林带中轨(低于中轨 1% 以上)
|
||
cond_a = dataframe['close_1h'] < dataframe['bb_middle_1h'] * 0.99
|
||
|
||
# 条件 b:STOCHRSI 超过90
|
||
cond_b = dataframe['stochrsi_k_1h'] > 90
|
||
|
||
# 条件 c:看跌蜡烛图形态
|
||
open_1h = dataframe['open_1h']
|
||
close_1h = dataframe['close_1h']
|
||
high_1h = dataframe['high_1h']
|
||
low_1h = dataframe['low_1h']
|
||
prev_open = open_1h.shift(1)
|
||
prev_close = close_1h.shift(1)
|
||
|
||
cond_engulfing = (
|
||
(prev_close > prev_open) &
|
||
(close_1h < open_1h) &
|
||
(close_1h < prev_open) &
|
||
(open_1h > prev_close)
|
||
)
|
||
cond_dark_cloud_cover = (
|
||
(prev_close > prev_open) &
|
||
(open_1h > prev_close) &
|
||
(close_1h < (prev_open + prev_close) / 2) &
|
||
(close_1h < open_1h)
|
||
)
|
||
body = abs(open_1h - close_1h)
|
||
upper_wick = high_1h - np.maximum(open_1h, close_1h)
|
||
lower_wick = np.minimum(open_1h, close_1h) - low_1h
|
||
cond_shooting_star = (upper_wick > 2 * body) & (lower_wick < body) & (close_1h < open_1h)
|
||
cond_stochrsi_high = self.is_stochrsi_overbought(dataframe, period=10, threshold=85)
|
||
cond_c = cond_engulfing | cond_dark_cloud_cover | cond_shooting_star | cond_stochrsi_high
|
||
|
||
# 条件 d:成交量显著下降
|
||
cond_d = dataframe['volume_z_score_1h'] < -1.0 if has_volume_z_score else pd.Series([False] * len(dataframe), index=dataframe.index)
|
||
|
||
# 综合熊市信号(至少满足两个条件)
|
||
bearish_signal = (cond_a & cond_b) | (cond_a & cond_c) | (cond_b & cond_c) | (cond_a & cond_d)
|
||
|
||
# 记录每个条件的触发情况
|
||
logger.info(f"[{pair}] 熊市信号 - 条件a (价格低于布林中轨): {cond_a.iloc[-1]}")
|
||
logger.info(f"[{pair}] 熊市信号 - 条件b (STOCHRSI>90): {cond_b.iloc[-1]}")
|
||
logger.info(f"[{pair}] 熊市信号 - 条件c (看跌形态): {cond_c.iloc[-1]}")
|
||
logger.info(f"[{pair}] 熊市信号 - 条件d (成交量下降): {cond_d.iloc[-1]}")
|
||
|
||
# 汇总所有条件的得分
|
||
bearish_score = 0
|
||
if cond_a.iloc[-1]:
|
||
bearish_score += 25
|
||
if cond_b.iloc[-1]:
|
||
bearish_score += 25
|
||
if cond_c.iloc[-1]:
|
||
bearish_score += 25
|
||
if cond_d.iloc[-1]:
|
||
bearish_score += 25
|
||
|
||
logger.info(f"[{pair}] 熊市信号总得分: {bearish_score}/100")
|
||
|
||
return bearish_signal
|
||
|
||
def trailing_space(self):
|
||
return [
|
||
DecimalParameter(0.01, 0.05, name="trailing_stop_start"),
|
||
DecimalParameter(0.005, 0.02, name="trailing_stop_distance")
|
||
]
|
||
|
||
def leverage_space(self):
|
||
return [
|
||
DecimalParameter(-0.05, -0.01, name="add_position_threshold", default=-0.02),
|
||
DecimalParameter(0.2, 0.7, name="exit_position_ratio", default=0.5),
|
||
IntParameter(1, 10, name="cooldown_period_minutes", default=5),
|
||
IntParameter(1, 3, name="max_entry_position_adjustment", default=2)
|
||
]
|
||
|
||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
"""
|
||
计算主时间框架(3m)和 1h 时间框架的指标,并映射到主 dataframe。
|
||
包含 FreqAI 预测、布林带、RSI、成交量 Z 分数等,并确保 1h 数据列完整性。
|
||
"""
|
||
pair = metadata.get('pair', 'Unknown')
|
||
logger.info(f"[{pair}] 当前可用列(调用FreqAI前):{list(dataframe.columns)}")
|
||
|
||
# 计算主时间框架(3m)指标
|
||
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
|
||
dataframe["price_value_divergence"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
|
||
upperband, middleband, lowerband = ta.BBANDS(dataframe["close"], timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
|
||
dataframe["bb_upperband"] = upperband
|
||
dataframe["bb_middleband"] = middleband
|
||
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"]
|
||
# 计算主时间框架的 STOCHRSI
|
||
stochrsi = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
|
||
dataframe["stochrsi_k"] = stochrsi["fastk"]
|
||
dataframe["stochrsi_d"] = stochrsi["fastd"]
|
||
|
||
# 获取 1h 时间框架数据
|
||
dataframe_1h = self.dp.get_pair_dataframe(pair=pair, timeframe="1h")
|
||
if dataframe_1h.empty or len(dataframe_1h) < 50:
|
||
logger.warning(f"[{pair}] 1h 数据为空或不足({len(dataframe_1h)} 根K线),初始化空列")
|
||
for col in ['open_1h', 'high_1h', 'low_1h', 'close_1h', 'stochrsi_k_1h', 'stochrsi_d_1h',
|
||
'bb_upper_1h', 'bb_middle_1h', 'bb_lower_1h', 'volume_z_score_1h']:
|
||
dataframe[col] = np.nan
|
||
else:
|
||
# 计算 1h 指标
|
||
if len(dataframe_1h) >= 20: # 确保有足够数据计算 rolling(20)
|
||
stochrsi_1h = ta.STOCHRSI(dataframe_1h, timeperiod=14, fastk_period=3, fastd_period=3)
|
||
dataframe_1h['stochrsi_k'] = stochrsi_1h['fastk']
|
||
dataframe_1h['stochrsi_d'] = stochrsi_1h['fastd']
|
||
real = ta.TYPPRICE(dataframe_1h)
|
||
upperband, middleband, lowerband = ta.BBANDS(real, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
|
||
dataframe_1h['bb_upper_1h'] = upperband
|
||
dataframe_1h['bb_middle_1h'] = middleband
|
||
dataframe_1h['bb_lower_1h'] = lowerband
|
||
dataframe_1h['volume_mean_20_1h'] = dataframe_1h["volume"].rolling(20).mean()
|
||
dataframe_1h['volume_std_20_1h'] = dataframe_1h["volume"].rolling(20).std()
|
||
dataframe_1h['volume_z_score_1h'] = (dataframe_1h["volume"] - dataframe_1h['volume_mean_20_1h']) / dataframe_1h['volume_std_20_1h']
|
||
# 清理 NaN 和无穷值
|
||
dataframe_1h['volume_z_score_1h'] = dataframe_1h['volume_z_score_1h'].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||
else:
|
||
logger.warning(f"[{pair}] 1h 数据不足以计算 volume_z_score_1h({len(dataframe_1h)} 根K线,需至少20根)")
|
||
dataframe_1h['volume_z_score_1h'] = np.nan
|
||
|
||
# 映射 1h 数据到主时间框架
|
||
for col in ['open', 'high', 'low', 'close', 'stochrsi_k', 'stochrsi_d', 'bb_upper_1h', 'bb_middle_1h', 'bb_lower_1h', 'volume_z_score_1h']:
|
||
if col in dataframe_1h.columns:
|
||
dataframe[col if col.endswith('_1h') else f"{col}_1h"] = dataframe_1h[col].reindex(dataframe.index, method='ffill').bfill()
|
||
else:
|
||
logger.warning(f"[{pair}] 1h 数据缺少列 {col},初始化为空")
|
||
dataframe[col if col.endswith('_1h') else f"{col}_1h"] = np.nan
|
||
|
||
# 数据清理:处理 NaN 和无穷值
|
||
for col in ["ema200", "bb_upperband", "bb_middleband", "bb_lowerband", "rsi", "volume_z_score",
|
||
"&-price_value_divergence", "price_value_divergence", "open_1h", "high_1h", "low_1h",
|
||
"close_1h", "stochrsi_k_1h", "stochrsi_d_1h", "bb_upper_1h", "bb_middle_1h", "bb_lower_1h",
|
||
"volume_z_score_1h"]:
|
||
if col in dataframe.columns:
|
||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||
|
||
# 调用 FreqAI 预测
|
||
if not hasattr(self, 'freqai') or self.freqai is None:
|
||
logger.error(f"[{pair}] FreqAI 未初始化,回退到规则计算")
|
||
dataframe["&-price_value_divergence"] = dataframe["price_value_divergence"]
|
||
else:
|
||
logger.info(f"[{pair}] 调用 FreqAI 预测,类型:{type(self.freqai)}")
|
||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||
if "&-price_value_divergence" not in dataframe.columns:
|
||
logger.warning(f"[{pair}] FreqAI 未生成 &-price_value_divergence,回退到规则计算")
|
||
dataframe["&-price_value_divergence"] = dataframe["price_value_divergence"]
|
||
|
||
# 计算 labels_mean 和 labels_std
|
||
labels_mean = None
|
||
labels_std = None
|
||
try:
|
||
model_base_dir = os.path.join(self.config["user_data_dir"], "models", self.freqai_info["identifier"])
|
||
pair_base = pair.split('/')[0] if '/' in pair else pair
|
||
sub_dirs = glob.glob(os.path.join(model_base_dir, f"sub-train-{pair_base}_*"))
|
||
if not sub_dirs:
|
||
logger.warning(f"[{pair}] 未找到任何子目录:{model_base_dir}/sub-train-{pair_base}_*")
|
||
else:
|
||
latest_sub_dir = max(sub_dirs, key=lambda x: int(x.split('_')[-1]))
|
||
pair_base_lower = pair_base.lower()
|
||
timestamp = latest_sub_dir.split('_')[-1]
|
||
metadata_file = os.path.join(latest_sub_dir, f"cb_{pair_base_lower}_{timestamp}_metadata.json")
|
||
if os.path.exists(metadata_file):
|
||
with open(metadata_file, "r") as f:
|
||
metadata = json.load(f)
|
||
labels_mean = metadata["labels_mean"]["&-price_value_divergence"]
|
||
labels_std = metadata["labels_std"]["&-price_value_divergence"]
|
||
logger.info(f"[{pair}] 从最新子目录 {latest_sub_dir} 读取 labels_mean:{labels_mean}, labels_std:{labels_std}")
|
||
else:
|
||
logger.warning(f"[{pair}] 最新的 metadata.json 文件 {metadata_file} 不存在")
|
||
except Exception as e:
|
||
logger.warning(f"[{pair}] 无法从子目录读取 labels_mean 和 labels_std:{e},重新计算")
|
||
|
||
if labels_mean is None or labels_std is None:
|
||
logger.warning(f"[{pair}] 无法获取 labels_mean 和 labels_std,重新计算")
|
||
dataframe["&-price_value_divergence_actual"] = (dataframe["close"] - dataframe["ema200"]) / dataframe["ema200"]
|
||
dataframe["&-price_value_divergence_actual"] = dataframe["&-price_value_divergence_actual"].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||
recent_data = dataframe["&-price_value_divergence_actual"].tail(self.fit_live_predictions_candles)
|
||
labels_mean = recent_data.mean()
|
||
labels_std = recent_data.std()
|
||
if np.isnan(labels_std) or labels_std == 0:
|
||
labels_std = 0.01
|
||
logger.warning(f"[{pair}] labels_std 计算异常,使用默认值 0.01")
|
||
|
||
# 根据市场趋势得分动态调整买卖阈值
|
||
market_trend_score = self.get_market_trend(dataframe=dataframe, metadata={'pair': pair})
|
||
k_buy = self.linear_map(market_trend_score, 0, 100, 1.2, 0.8)
|
||
k_sell = self.linear_map(market_trend_score, 0, 100, 1.5, 1.0)
|
||
|
||
self.buy_threshold = labels_mean - k_buy * labels_std
|
||
self.sell_threshold = labels_mean + k_sell * labels_std
|
||
|
||
# 使用 Hyperopt 参数限制阈值
|
||
self.buy_threshold = max(self.buy_threshold, self.BUY_THRESHOLD_MIN.value)
|
||
self.buy_threshold = min(self.buy_threshold, self.BUY_THRESHOLD_MAX.value)
|
||
self.sell_threshold = min(self.sell_threshold, self.SELL_THRESHOLD_MAX.value)
|
||
self.sell_threshold = max(self.sell_threshold, self.SELL_THRESHOLD_MIN.value)
|
||
|
||
# 调试日志
|
||
logger.info(f"[{pair}] 市场趋势得分:{market_trend_score}, labels_mean:{labels_mean:.4f}, labels_std:{labels_std:.4f}")
|
||
logger.info(f"[{pair}] k_buy:{k_buy:.2f}, k_sell:{k_sell:.2f}")
|
||
logger.info(f"[{pair}] 动态买入阈值:{self.buy_threshold:.4f}, 卖出阈值:{self.sell_threshold:.4f}")
|
||
logger.info(f"[{pair}] 最新数据 - close: {dataframe['close'].iloc[-1]:.6f}, "
|
||
f"rsi: {dataframe['rsi'].iloc[-1]:.2f}, "
|
||
f"&-price_value_divergence: {dataframe['&-price_value_divergence'].iloc[-1]:.6f}, "
|
||
f"volume_z_score: {dataframe['volume_z_score'].iloc[-1]:.2f}, "
|
||
f"bb_lowerband: {dataframe['bb_lowerband'].iloc[-1]:.6f}, "
|
||
f"close_1h: {dataframe['close_1h'].iloc[-1] if 'close_1h' in dataframe else 'N/A'}, "
|
||
f"stochrsi_k_1h: {dataframe['stochrsi_k_1h'].iloc[-1] if 'stochrsi_k_1h' in dataframe else 'N/A'}, "
|
||
f"volume_z_score_1h: {dataframe['volume_z_score_1h'].iloc[-1] if 'volume_z_score_1h' in dataframe else 'N/A'}")
|
||
|
||
if not self.stats_logged:
|
||
logger.info("===== 所有币对的 labels_mean 和 labels_std 汇总 =====")
|
||
for p, stats in self.pair_stats.items():
|
||
logger.info(f"[{p}] labels_mean:{stats['labels_mean']:.4f}, labels_std:{stats['labels_std']:.4f}")
|
||
logger.info("==============================================")
|
||
self.stats_logged = True
|
||
|
||
return dataframe
|
||
|
||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
pair = metadata.get('pair', 'Unknown')
|
||
conditions = []
|
||
logger.info(f"[{pair}] populate_entry_trend 被调用,数据行数:{len(dataframe)},时间:{dataframe.index[-1]}")
|
||
# 获取市场趋势得分
|
||
trend_score = self.get_market_trend(dataframe=dataframe, metadata=metadata)
|
||
|
||
# 动态调整成交量和 RSI 阈值
|
||
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 = 35
|
||
rsi_max = 55
|
||
rsi_threshold = self.linear_map(trend_score, 0, 100, rsi_max, rsi_min)
|
||
stochrsi_min = 25
|
||
stochrsi_max = 45
|
||
stochrsi_threshold = self.linear_map(trend_score, 0, 100, stochrsi_max, stochrsi_min)
|
||
|
||
# 计算熊市信号和 STOCHRSI 超买信号
|
||
bearish_signal = self.is_bearish_market(dataframe, metadata, timeframe="1h")
|
||
bearish_signal_aligned = bearish_signal.reindex(dataframe.index, method='ffill').fillna(False)
|
||
stochrsi_overbought = self.is_stochrsi_overbought(dataframe, period=10, threshold=85)
|
||
stochrsi_overbought_aligned = stochrsi_overbought.reindex(dataframe.index, method='ffill').fillna(True)
|
||
|
||
# 买入条件
|
||
if "&-price_value_divergence" in dataframe.columns:
|
||
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)
|
||
cond6 = ~bearish_signal_aligned
|
||
cond7 = ~stochrsi_overbought_aligned
|
||
|
||
# 记录 cond6 和 cond7 的触发情况
|
||
logger.info(f"[{pair}] cond6 (非熊市) 触发率: {(~bearish_signal_aligned).mean():.2%}, "
|
||
f"熊市信号 K 线数: {bearish_signal_aligned.sum()}")
|
||
logger.info(f"[{pair}] cond7 (STOCHRSI 非超买) 触发率: {(~stochrsi_overbought_aligned).mean():.2%}, "
|
||
f"STOCHRSI 超买 K 线数: {stochrsi_overbought_aligned.sum()}")
|
||
|
||
buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7 & (trend_score > 60)
|
||
conditions.append(buy_condition)
|
||
|
||
# 调试日志
|
||
divergence_value = dataframe["&-price_value_divergence"].iloc[-1]
|
||
volume_z_score_value = dataframe["volume_z_score"].iloc[-1]
|
||
rsi_value = dataframe["rsi"].iloc[-1]
|
||
stochrsi_value = dataframe["stochrsi_k"].iloc[-1]
|
||
bb_close_value = dataframe["close"].iloc[-1]
|
||
bb_lower_value = dataframe["bb_lowerband"].iloc[-1]
|
||
# 定义条件名称和状态
|
||
conditions_summary = [
|
||
("&-price_value_divergence", divergence_value, "<", self.buy_threshold, cond1.iloc[-1]),
|
||
("volume_z_score", volume_z_score_value, ">", volume_z_score_threshold, cond2.iloc[-1]),
|
||
("rsi", rsi_value, "<", rsi_threshold, cond3.iloc[-1]),
|
||
("close <= bb_lowerband", bb_close_value, "<=", bb_lower_value, cond4.iloc[-1]),
|
||
("stochrsi_k", stochrsi_value, "<", stochrsi_threshold, cond5.iloc[-1]),
|
||
("非熊市", None, None, None, cond6.iloc[-1]),
|
||
("STOCHRSI未持续超买", None, None, None, cond7.iloc[-1]),
|
||
]
|
||
|
||
# 输出每个条件的状态
|
||
logger.info(f"[{pair}] === 买入条件检查 ===")
|
||
failed_conditions = []
|
||
for name, value, operator, threshold, result in conditions_summary:
|
||
status = "✅" if result else "❌"
|
||
if value is not None and threshold is not None:
|
||
logger.info(f"[{pair}] {status} {name}: {value:.6f} {operator} {threshold:.6f}")
|
||
else:
|
||
logger.info(f"[{pair}] {status} {name}")
|
||
if not result:
|
||
failed_conditions.append(name)
|
||
else:
|
||
logger.warning(f"[{pair}] &-price_value_divergence 列缺失,跳过买入信号生成")
|
||
|
||
if conditions:
|
||
combined_condition = reduce(lambda x, y: x & y, conditions)
|
||
dataframe.loc[combined_condition, 'enter_long'] = 1
|
||
|
||
# 创建条件总结列表
|
||
conditions_summary = [
|
||
("&-price_value_divergence", divergence_value, "<", self.buy_threshold, cond1.iloc[-1]),
|
||
("volume_z_score", volume_z_score_value, ">", volume_z_score_threshold, cond2.iloc[-1]),
|
||
("rsi", rsi_value, "<", rsi_threshold, cond3.iloc[-1]),
|
||
("close <= bb_lowerband", bb_close_value, "<=", bb_lower_value, cond4.iloc[-1]),
|
||
("stochrsi_k", stochrsi_value, "<", stochrsi_threshold, cond5.iloc[-1]),
|
||
("非熊市", None, None, None, cond6.iloc[-1]),
|
||
("STOCHRSI未持续超买", None, None, None, cond7.iloc[-1]),
|
||
]
|
||
|
||
# 输出每个条件的状态
|
||
logger.info(f"[{pair}] === 买入条件检查 ===")
|
||
satisfied_conditions = []
|
||
for name, value, operator, threshold, result in conditions_summary:
|
||
status = "✅" if result else "❌"
|
||
if value is not None and threshold is not None:
|
||
logger.info(f"[{pair}] {status} {name}: {value:.6f} {operator} {threshold:.6f}")
|
||
else:
|
||
logger.info(f"[{pair}] {status} {name}")
|
||
if result:
|
||
satisfied_conditions.append(name)
|
||
|
||
# 总结满足的条件
|
||
if combined_condition.any():
|
||
logger.info(f"[{pair}] ✅ 买入信号触发,满足条件: {', '.join(satisfied_conditions)},趋势得分:{trend_score:.2f}")
|
||
else:
|
||
logger.info(f"[{pair}] ❌ 买入条件未满足")
|
||
|
||
else:
|
||
logger.info(f"[{pair}] 无有效买入条件")
|
||
|
||
# 记录各条件触发率
|
||
logger.info(f"[{pair}] 各条件触发率 - "
|
||
f"cond1: {cond1.mean():.2%}, "
|
||
f"cond2: {cond2.mean():.2%}, "
|
||
f"cond3: {cond3.mean():.2%}, "
|
||
f"cond4: {cond4.mean():.2%}, "
|
||
f"cond5: {cond5.mean():.2%}, "
|
||
f"buy_condition: {buy_condition.mean():.2%}")
|
||
# 记录 enter_long 信号统计
|
||
logger.info(f"[{pair}] enter_long 信号总数:{dataframe['enter_long'].sum() if 'enter_long' in dataframe.columns else 0}")
|
||
|
||
return dataframe
|
||
|
||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
pair = metadata.get('pair', 'Unknown')
|
||
conditions = []
|
||
|
||
if "&-price_value_divergence" in dataframe.columns:
|
||
# 计算额外指标:StochRSI、ADX 和短期价格变化
|
||
stochrsi = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
|
||
dataframe["stochrsi_k"] = stochrsi["fastk"]
|
||
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
|
||
# 计算短期价格涨幅(最近 5 根 K 线,约 15 分钟)
|
||
dataframe["short_term_return"] = dataframe["close"].pct_change(5) * 100 # 百分比回报
|
||
|
||
# 获取市场趋势得分
|
||
trend_score = self.get_market_trend(dataframe=dataframe, metadata={'pair': pair})
|
||
|
||
# 条件 1:高阈值 &-price_value_divergence
|
||
cond1 = (
|
||
(dataframe["&-price_value_divergence"] > self.sell_threshold * 1.06) & # 提高到 1.06
|
||
(dataframe["adx"] > 20) # 趋势强度过滤
|
||
)
|
||
|
||
# 条件 2:超买信号
|
||
cond2 = (
|
||
(dataframe["rsi"] > 65) &
|
||
(dataframe["stochrsi_k"] > 70) & # StochRSI 超买
|
||
(dataframe["adx"] > 25) # 趋势强度
|
||
)
|
||
|
||
# 条件 3:快速拉升退出
|
||
# 检测最近 5 根 K 线(约 15 分钟)涨幅超过 3%,且有最低 2% 利润,结合 StochRSI 超买
|
||
min_profit = 0.02 # 最低利润 2%
|
||
rapid_rise_threshold = self.linear_map(trend_score, 0, 100, 3.5, 2) # 熊市 4%,牛市 2.5%
|
||
cond3 = (
|
||
(dataframe["short_term_return"] > rapid_rise_threshold) & # 短期快速拉升
|
||
(dataframe["close"] / dataframe["close"].shift(5) - 1 > min_profit) & # 确保最低利润
|
||
(dataframe["stochrsi_k"] > 70) & # 超买确认
|
||
(dataframe["volume_z_score"] > 1.0)
|
||
)
|
||
|
||
# 综合卖出条件:根据 trend_score 调整逻辑
|
||
trend_sell_threshold = 85
|
||
if trend_score > trend_sell_threshold:
|
||
sell_condition = (cond1 & cond2) | (cond1 & cond3) | (cond2 & cond3) # 中等趋势,至少两个条件满足
|
||
logger.info(f"[{pair}] 趋势得分 {trend_score:.2f} > 55,需满足至少两个条件")
|
||
else:
|
||
sell_condition = cond1 | cond2 | cond3 # 弱势趋势,任一条件满足
|
||
logger.info(f"[{pair}] 趋势得分 {trend_score:.2f} <= 趋势得分卖出条件:{trend_sell_threshold},任一条件满足")
|
||
|
||
conditions.append(sell_condition)
|
||
|
||
# 调试日志
|
||
divergence_value = dataframe["&-price_value_divergence"].iloc[-1] if not dataframe["&-price_value_divergence"].isna().all() else np.nan
|
||
rsi_value = dataframe["rsi"].iloc[-1] if not dataframe["rsi"].isna().all() else np.nan
|
||
stochrsi_value = dataframe["stochrsi_k"].iloc[-1] if not dataframe["stochrsi_k"].isna().all() else np.nan
|
||
adx_value = dataframe["adx"].iloc[-1] if not dataframe["adx"].isna().all() else np.nan
|
||
short_term_return = dataframe["short_term_return"].iloc[-1] if not dataframe["short_term_return"].isna().all() else np.nan
|
||
logger.info(f"[{pair}] 卖出条件检查 - "
|
||
f"&-price_value_divergence={divergence_value:.6f} > {self.sell_threshold * 1.06:.6f}: {cond1.iloc[-1]}, "
|
||
f"rsi={rsi_value:.2f} > 75 & stochrsi_k={stochrsi_value:.2f} > 80: {cond2.iloc[-1]}, "
|
||
f"short_term_return={short_term_return:.2f}% > {rapid_rise_threshold:.2f}% & profit > {min_profit*100:.2f}%: {cond3.iloc[-1]}, "
|
||
f"adx={adx_value:.2f}, trend_score={trend_score:.2f}")
|
||
else:
|
||
logger.warning(f"[{pair}] ⚠️ &-price_value_divergence 列缺失,跳过该条件")
|
||
|
||
if len(conditions) > 0:
|
||
dataframe.loc[reduce(lambda x, y: x & y, conditions), 'exit_long'] = 1
|
||
logger.info(f"[{pair}] 出场信号触发,条件满足,趋势得分:{trend_score:.2f}")
|
||
else:
|
||
logger.info(f"[{pair}] 无有效卖出条件")
|
||
|
||
return dataframe
|
||
|
||
def buy_space(self):
|
||
return [
|
||
DecimalParameter(-0.1, -0.01, name="buy_threshold_min"),
|
||
DecimalParameter(-0.02, -0.001, name="buy_threshold_max"),
|
||
DecimalParameter(-0.05, -0.01, name="add_position_threshold", default=-0.02),
|
||
IntParameter(1, 10, name="cooldown_period_minutes", default=5),
|
||
IntParameter(1, 3, name="max_entry_position_adjustment", default=2)
|
||
]
|
||
|
||
def sell_space(self):
|
||
return [
|
||
DecimalParameter(0.001, 0.02, name="sell_threshold_min"),
|
||
DecimalParameter(0.02, 0.1, name="sell_threshold_max"),
|
||
DecimalParameter(0.2, 0.7, name="exit_position_ratio", default=0.5)
|
||
]
|
||
|
||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||
current_rate: float, current_profit: float,
|
||
min_stake: float | None, max_stake: float,
|
||
current_entry_rate: float, current_exit_rate: float,
|
||
current_entry_profit: float, current_exit_profit: float,
|
||
**kwargs) -> float | None | tuple[float | None, str | None]:
|
||
"""
|
||
动态调整仓位:支持加仓、减仓、追踪止损和最大持仓时间限制
|
||
参数:
|
||
- trade: 当前交易对象
|
||
- current_time: 当前时间
|
||
- current_rate: 当前价格
|
||
- current_profit: 当前总盈利
|
||
- min_stake: 最小下注金额
|
||
- max_stake: 最大下注金额
|
||
- current_entry_rate: 当前入场价格
|
||
- current_exit_rate: 当前退出价格
|
||
- current_entry_profit: 当前入场盈利
|
||
- current_exit_profit: 当前退出盈利
|
||
返回:
|
||
- 调整金额(正数为加仓,负数为减仓)或 None
|
||
"""
|
||
pair = trade.pair
|
||
dataframe = self.dp.get_pair_dataframe(pair, self.timeframe)
|
||
trend_score = self.get_market_trend(dataframe=dataframe, metadata={'pair': pair})
|
||
hold_time = (current_time - trade.open_date_utc).total_seconds() / 60
|
||
profit_ratio = (current_rate - trade.open_rate) / trade.open_rate
|
||
|
||
initial_stake_amount = trade.stake_amount / 3
|
||
logger.info(f"{pair} 首次入场金额: {initial_stake_amount:.2f}, 当前持仓金额: {trade.stake_amount:.2f}, "
|
||
f"加仓次数: {trade.nr_of_successful_entries - 1}")
|
||
|
||
# 加仓逻辑
|
||
max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT.value
|
||
if trade.nr_of_successful_entries <= max_entry_adjustments + 1:
|
||
add_position_threshold = self.ADD_POSITION_THRESHOLD.value
|
||
# 线性映射加仓阈值,趋势值越高,加仓越严格
|
||
add_threshold = 80 - 30 * (trend_score / 100) # 趋势值 100 -> 50, 0 -> 80
|
||
if profit_ratio <= add_position_threshold and hold_time > 5 and trend_score <= add_threshold:
|
||
logger.info(f"{pair} 初始下注金额: {initial_stake_amount:.2f}, trend_score: {trend_score:.2f}, add_threshold: {add_threshold} ")
|
||
|
||
# 计算加仓金额
|
||
add_count = trade.nr_of_successful_entries - 1
|
||
multipliers = [2, 4, 8]
|
||
if add_count < len(multipliers):
|
||
multiplier = multipliers[add_count]
|
||
add_amount = initial_stake_amount * multiplier
|
||
logger.info(f"{pair} 第 {add_count + 1} 次加仓,倍数={multiplier}, "
|
||
f"金额 = {initial_stake_amount:.2f} * {multiplier} = {add_amount:.2f}")
|
||
logger.info(f"{pair} 加仓计算: 第 {add_count + 1} 次加仓,倍数={multiplier}, "
|
||
f"金额 = {initial_stake_amount:.2f} * {multiplier} = {add_amount:.2f}")
|
||
|
||
if min_stake is not None and add_amount < min_stake:
|
||
logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 低于最小下注金额 {min_stake:.2f},取消加仓")
|
||
return (None, f"Add amount {add_amount:.2f} below min_stake {min_stake:.2f}")
|
||
if add_amount > max_stake:
|
||
logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 超出最大可用金额 {max_stake:.2f},调整为 {max_stake:.2f}")
|
||
add_amount = max_stake
|
||
logger.info(f"{pair} 价格下跌 {profit_ratio*100:.2f}%,触发第 {add_count + 1} 次加仓 {add_amount:.2f}")
|
||
return (add_amount, f"Price dropped {profit_ratio*100:.2f}%, add {add_amount:.2f}")
|
||
|
||
# 减仓逻辑
|
||
exit_position_ratio = self.EXIT_POSITION_RATIO.value
|
||
if profit_ratio >= 0.03:
|
||
# 趋势值越高,减仓比例越低
|
||
reduce_factor = 0.6 + 0.4 * (1 - trend_score / 100) # 牛市(100) -> 0.6, 熊市(0) -> 1.0
|
||
reduce_amount = -exit_position_ratio * reduce_factor * trade.stake_amount
|
||
logger.info(f"{pair} 趋势值 {trend_score:.2f},利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}")
|
||
return (reduce_amount, f"Profit {profit_ratio*100:.2f}%")
|
||
elif profit_ratio >= 0.05:
|
||
reduce_factor = 1.4 - 0.4 * (trend_score / 100) # 牛市(100) -> 1.0, 熊市(0) -> 1.4
|
||
reduce_amount = -exit_position_ratio * reduce_factor * trade.stake_amount
|
||
logger.info(f"{pair} 趋势值 {trend_score:.2f},利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f}")
|
||
return (reduce_amount, f"Profit {profit_ratio*100:.2f}%")
|
||
|
||
# 追踪止损逻辑
|
||
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
|
||
|
||
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
|
||
|
||
if self.trailing_stop_enabled:
|
||
max_rate = trade.max_rate or current_rate
|
||
trailing_stop_price = max_rate * (1 - trailing_stop_distance)
|
||
if current_rate < trailing_stop_price:
|
||
logger.info(f"{pair} 价格回落至 {trailing_stop_price:.6f},触发全部卖出")
|
||
return (-trade.stake_amount, f"Trailing stop at {trailing_stop_price:.6f}")
|
||
trade.adjust_min_max_rates(current_rate, trade.min_rate)
|
||
return None
|
||
|
||
return None
|
||
|
||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
||
# 调试日志:记录输入参数
|
||
logger.info(f"[{pair}] confirm_trade_entry called with rate={rate}, type(rate)={type(rate)}, "
|
||
f"amount={amount}, order_type={order_type}, time_in_force={time_in_force}")
|
||
|
||
# 检查 rate 是否有效
|
||
if not isinstance(rate, (float, int)) or rate is None:
|
||
logger.error(f"[{pair}] Invalid rate value: {rate} (type: {type(rate)}). Skipping trade entry.")
|
||
return False
|
||
|
||
# 获取当前数据
|
||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||
last_candle = dataframe.iloc[-1]
|
||
|
||
market_trend_score = self.get_market_trend(dataframe=DataFrame, metadata={'pair': pair})
|
||
cooldown_period_minutes = self.COOLDOWN_PERIOD_MINUTES.value if market_trend_score > 50 else self.COOLDOWN_PERIOD_MINUTES.value // 2
|
||
|
||
if pair in self.last_entry_time:
|
||
last_time = self.last_entry_time[pair]
|
||
if (current_time - last_time).total_seconds() < cooldown_period_minutes * 60:
|
||
logger.info(f"[{pair}] 冷却期内({cooldown_period_minutes} 分钟),跳过本次入场")
|
||
return False
|
||
|
||
self.last_entry_time[pair] = current_time
|
||
self.trailing_stop_enabled = False
|
||
try:
|
||
logger.info(f"[{pair}] 确认入场,价格:{float(rate):.6f}")
|
||
except (ValueError, TypeError) as e:
|
||
logger.error(f"[{pair}] Failed to format rate: {rate} (type: {type(rate)}), error: {e}")
|
||
return False
|
||
|
||
return True
|
||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||
rate: float, time_in_force: str, exit_reason: str,
|
||
current_time: datetime, **kwargs) -> bool:
|
||
adjusted_rate = rate * (1 + 0.0025)
|
||
logger.info(f"[{pair}] 退出交易,原因:{exit_reason}, 原始利润:{trade.calc_profit_ratio(rate):.2%},"f"调整后卖出价:{adjusted_rate:.6f}")
|
||
return True
|
||
|
||
def custom_roi(self, trade: Trade, current_profit: float, current_time: datetime, trade_dur: int,
|
||
current_rate: float = None, min_stake: float | None = None, max_stake: float | None = None) -> dict:
|
||
"""
|
||
动态调整 ROI 表格,基于 FreqAI 预测的 &-price_value_divergence 和 RSI。
|
||
- 负的 divergence(预测上涨)或低 RSI 时提高 ROI。
|
||
- 正的 divergence(预测下跌)或高 RSI 时降低 ROI。
|
||
- 长时间持仓降低 ROI 目标。
|
||
"""
|
||
pair = trade.pair
|
||
logger.info(f"[{pair}] 计算自定义 ROI,当前盈利: {current_profit:.2%}, 持仓时间: {trade_dur} 分钟")
|
||
|
||
# 获取最新数据
|
||
dataframe = self.dp.get_pair_dataframe(pair=pair, timeframe=self.timeframe)
|
||
dataframe = self.populate_indicators(dataframe, {'pair': pair}) # 计算指标
|
||
|
||
# 获取 FreqAI 预测和 RSI
|
||
divergence = dataframe["&-price_value_divergence"].iloc[-1] if "&-price_value_divergence" in dataframe else 0
|
||
rsi = dataframe["rsi"].iloc[-1] if "rsi" in dataframe else 50
|
||
|
||
# 计算调整系数
|
||
# 1. Divergence 调整:负值(预测上涨)-> 提高 ROI,正值(预测下跌)-> 降低 ROI
|
||
divergence_factor = self.linear_map(divergence, -0.1, 0.1, 1.2, 0.8)
|
||
|
||
# 2. RSI 调整:低 RSI(超卖)-> 提高 ROI,高 RSI(超买)-> 降低 ROI
|
||
rsi_factor = self.linear_map(rsi, 30, 70, 1.2, 0.8)
|
||
|
||
# 3. 时间调整:持仓时间越长,ROI 目标降低
|
||
time_factor = self.linear_map(trade_dur, 0, 240, 1.0, 0.7) # 4小时后 ROI 降低到 70%
|
||
|
||
# 综合调整系数
|
||
roi_factor = divergence_factor * rsi_factor * time_factor
|
||
|
||
# 默认 ROI 表格
|
||
base_roi = {
|
||
0: 0.06,
|
||
30: 0.04,
|
||
90: 0.025,
|
||
270: 0.002
|
||
}
|
||
|
||
# 动态调整 ROI,限制在 0% 到 20% 之间
|
||
dynamic_roi = {time: min(max(roi * roi_factor, 0.0), 0.2) for time, roi in base_roi.items()}
|
||
|
||
logger.info(f"[{pair}] Divergence: {divergence:.4f}, RSI: {rsi:.2f}, 持仓时间: {trade_dur} 分钟, "
|
||
f"调整系数: divergence={divergence_factor:.2f}, rsi={rsi_factor:.2f}, time={time_factor:.2f}, "
|
||
f"总系数={roi_factor:.2f}, 动态 ROI 表格: {dynamic_roi}")
|
||
return dynamic_roi
|
||
|
||
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
|
||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||
adjusted_rate = proposed_rate * (1 - 0.0075)
|
||
logger.info(f"[{pair}] 自定义买入价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f})")
|
||
return adjusted_rate
|
||
|
||
def custom_exit_price(self, pair: str, trade: Trade,
|
||
current_time: datetime, proposed_rate: float,
|
||
current_profit: float, exit_tag: str | None, **kwargs) -> float:
|
||
# 获取趋势得分
|
||
trend_score = self.get_market_trend(metadata={'pair': pair})
|
||
# 计算额外的百分比提升
|
||
extra_percentage_increase = max(0, trend_score - 90) / 100
|
||
# 调整卖出价格
|
||
adjusted_rate = proposed_rate * (1 + extra_percentage_increase)
|
||
logger.info(f"[{pair}] 自定义卖出价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f}),趋势得分:{trend_score},额外提升:{extra_percentage_increase * 100:.2f}%")
|
||
return adjusted_rate
|
||
|
||
def get_market_trend(self, dataframe: DataFrame = None, metadata: dict = None) -> int:
|
||
try:
|
||
timeframes = ["3m", "15m", "1h"]
|
||
weights = {"3m": 0.3, "15m": 0.35, "1h": 0.35}
|
||
trend_scores = {}
|
||
pair = metadata.get('pair', 'Unknown') if metadata else 'Unknown'
|
||
|
||
# 检查 pair 是否有效
|
||
if pair == 'Unknown':
|
||
logger.error(f"[{pair}] Invalid pair in metadata: {metadata}. Returning default score 50")
|
||
return 50
|
||
|
||
logger.info(f"[{pair}] 正在计算多时间框架市场趋势得分")
|
||
|
||
for tf in timeframes:
|
||
# 优先使用传入的 dataframe(如果匹配主时间框架),否则加载目标币对数据
|
||
pair_df = dataframe if tf == self.timeframe and dataframe is not None else self.dp.get_pair_dataframe(pair=pair, timeframe=tf)
|
||
|
||
min_candles = 200 if tf == "3m" else 100 if tf == "15m" else 50
|
||
if pair_df.empty or len(pair_df) < min_candles:
|
||
logger.warning(f"[{pair}] 数据不足({tf}),使用默认得分:50")
|
||
trend_scores[tf] = 50
|
||
continue
|
||
|
||
# 价格趋势
|
||
ema_short_period = 50 if tf == "3m" else 20 if tf == "15m" else 12
|
||
ema_long_period = 200 if tf == "3m" else 80 if tf == "15m" else 50
|
||
pair_df["ema_short"] = ta.EMA(pair_df, timeperiod=ema_short_period)
|
||
pair_df["ema_long"] = ta.EMA(pair_df, timeperiod=ema_long_period)
|
||
pair_df["ema_short_slope"] = (pair_df["ema_short"] - pair_df["ema_short"].shift(10)) / pair_df["ema_short"].shift(10)
|
||
|
||
price_above_ema = pair_df["close"].iloc[-1] > pair_df["ema_long"].iloc[-1]
|
||
ema_short_above_ema_long = pair_df["ema_short"].iloc[-1] > pair_df["ema_long"].iloc[-1]
|
||
ema_short_slope = pair_df["ema_short_slope"].iloc[-1]
|
||
|
||
price_score = 0
|
||
if price_above_ema:
|
||
price_score += 20
|
||
if ema_short_above_ema_long:
|
||
price_score += 20
|
||
if ema_short_slope > 0.005:
|
||
price_score += 15
|
||
elif ema_short_slope < -0.005:
|
||
price_score -= 15
|
||
|
||
# K线形态
|
||
pair_df["bullish_engulfing"] = (
|
||
(pair_df["close"].shift(1) < pair_df["open"].shift(1)) &
|
||
(pair_df["close"] > pair_df["open"]) &
|
||
(pair_df["close"] > pair_df["open"].shift(1)) &
|
||
(pair_df["open"] < pair_df["close"].shift(1))
|
||
).fillna(False)
|
||
pair_df["bearish_engulfing"] = (
|
||
(pair_df["close"].shift(1) > pair_df["open"].shift(1)) &
|
||
(pair_df["close"] < pair_df["open"]) &
|
||
(pair_df["close"] < pair_df["open"].shift(1)) &
|
||
(pair_df["open"] > pair_df["close"].shift(1))
|
||
).fillna(False)
|
||
|
||
kline_score = 0
|
||
if pair_df["bullish_engulfing"].iloc[-1]:
|
||
kline_score += 15
|
||
elif pair_df["bearish_engulfing"].iloc[-1]:
|
||
kline_score -= 15
|
||
volatility = pair_df["close"].pct_change(10).std() * 100
|
||
if volatility > 0.5:
|
||
kline_score += 10 if price_score > 0 else -10
|
||
|
||
# StochRSI
|
||
stochrsi = ta.STOCHRSI(pair_df, timeperiod=14, fastk_period=3, fastd_period=3)
|
||
pair_df["stochrsi_k"] = stochrsi["fastk"]
|
||
pair_df["stochrsi_d"] = stochrsi["fastd"]
|
||
|
||
stochrsi_score = 0
|
||
stochrsi_k = pair_df["stochrsi_k"].iloc[-1]
|
||
stochrsi_d = pair_df["stochrsi_d"].iloc[-1]
|
||
if stochrsi_k > 80 and stochrsi_k < stochrsi_d:
|
||
stochrsi_score -= 15
|
||
elif stochrsi_k < 20 and stochrsi_k > stochrsi_d:
|
||
stochrsi_score += 15
|
||
elif stochrsi_k > 50:
|
||
stochrsi_score += 5
|
||
elif stochrsi_k < 50:
|
||
stochrsi_score -= 5
|
||
|
||
# 量价关系
|
||
pair_df["volume_mean_20"] = pair_df["volume"].rolling(20).mean()
|
||
pair_df["volume_std_20"] = pair_df["volume"].rolling(20).std()
|
||
pair_df["volume_z_score"] = (pair_df["volume"] - pair_df["volume_mean_20"]) / pair_df["volume_std_20"]
|
||
pair_df["adx"] = ta.ADX(pair_df, timeperiod=14)
|
||
|
||
volume_score = 0
|
||
if pair_df["volume_z_score"].iloc[-1] > 1.5:
|
||
volume_score += 10 if price_score > 0 else -10
|
||
if pair_df["adx"].iloc[-1] > 25:
|
||
volume_score += 10 if price_score > 0 else -10
|
||
|
||
# 综合得分
|
||
raw_score = price_score + kline_score + stochrsi_score + volume_score
|
||
raw_score = max(min(raw_score, 50), -50)
|
||
|
||
# 对数映射到 [0, 100]
|
||
if raw_score >= 0:
|
||
mapped_score = 50 + 50 * (np.log1p(raw_score / 50) / np.log1p(1))
|
||
else:
|
||
mapped_score = 50 * (np.log1p(-raw_score / 50) / np.log1p(1))
|
||
|
||
trend_scores[tf] = max(0, min(100, int(round(mapped_score))))
|
||
logger.info(f"[{pair}] {tf} 趋势得分:{trend_scores[tf]}, 原始得分:{raw_score}, "
|
||
f"价格得分:{price_score}, K线得分:{kline_score}, "
|
||
f"StochRSI得分:{stochrsi_score}, 量价得分:{volume_score}")
|
||
|
||
# 动态调整权重
|
||
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.info(f"[{pair}] 1h 趋势得分({trend_scores.get('1h', 50)})显著高于 3m({trend_scores.get('3m', 50)}),调整权重为 {weights}")
|
||
|
||
# 加权融合
|
||
final_score = sum(trend_scores[tf] * weights[tf] for tf in timeframes)
|
||
final_score = int(round(final_score))
|
||
final_score = max(0, min(100, final_score))
|
||
|
||
logger.info(f"[{pair}] 最终趋势得分:{final_score}, "
|
||
f"3m得分:{trend_scores.get('3m', 50)}, 15m得分:{trend_scores.get('15m', 50)}, "
|
||
f"1h得分:{trend_scores.get('1h', 50)}")
|
||
return final_score
|
||
|
||
except Exception as e:
|
||
logger.error(f"[{pair}] 获取市场趋势失败:{e}", exc_info=True)
|
||
return 50
|