Files
Chan/strategies/CryptoFuturesAIStrategy.py
2026-03-06 22:08:24 +08:00

286 lines
11 KiB
Python

# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
import logging
from functools import reduce
from datetime import datetime
from typing import Optional
import numpy as np
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy, merge_informative_pair
from freqtrade.persistence import Trade
# freqtrade backtesting -c ./user_data/Chan/config/FreqAI_Test.json --strategy CryptoFuturesAIStrategy --strategy-path ./user_data/Chan/strategies --freqaimodel LightGBMRegressor --timerange=20260201-
# freqtrade trade -c ./user_data/Chan/config/FreqAI_Test.json --strategy CryptoFuturesAIStrategy --strategy-path ./user_data/Chan/strategies --freqaimodel LightGBMRegressor
logger = logging.getLogger(__name__)
class CryptoFuturesAIStrategy(IStrategy):
"""
SOL/USDT 合约 AI 策略 - FreqAI + LightGBM
核心思路:
1. 用 FreqAI 的 LightGBM 回归模型预测未来价格变化方向和幅度
2. 模型自动在滚动窗口上重新训练,适应市场变化
3. 结合传统技术指标作为特征输入,让 AI 学习最优组合
4. 用 z-score 动态阈值代替固定参数,自适应不同市场环境
5. 保留 trailing_stop 作为风控(这是原策略的盈利核心)
相比固定参数策略的优势:
- 参数自适应:模型每隔一段时间重新训练,适应市场状态变化
- 特征自动选择:LightGBM 自动学习哪些指标在当前市场最有用
- 动态阈值:用预测值的统计分布来决定入场,而非固定数值
- 多维度输入:同时考虑价格、成交量、波动率、时间等多维信息
"""
INTERFACE_VERSION = 3
timeframe = '5m' # FreqAI 用5分钟作为基础时间框架,更稳定
can_short = True
# === 风控参数(保留原策略的盈利核心) ===
stoploss = -0.025
trailing_stop = True
trailing_stop_positive = 0.008
trailing_stop_positive_offset = 0.015
trailing_only_offset_is_reached = True
use_custom_stoploss = False
use_exit_signal = False # 禁用exit_signal,让trailing_stop管理退出
process_only_new_candles = True
startup_candle_count: int = 100 # 需要足够的历史数据计算指标
# =====================================================
# FreqAI 特征工程函数
# =====================================================
def feature_engineering_expand_all(
self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
) -> DataFrame:
"""
自动扩展特征 - 精简版,减少特征数量防止内存溢出
"""
# 核心动量指标
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
# 相对成交量
dataframe["%-relative_volume-period"] = (
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
)
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"]
# K线形态特征
dataframe["%-candle_body"] = (
(dataframe["close"] - dataframe["open"]) / dataframe["open"]
)
dataframe["%-upper_shadow"] = (
(dataframe["high"] - dataframe[["open", "close"]].max(axis=1))
/ dataframe["close"]
)
dataframe["%-lower_shadow"] = (
(dataframe[["open", "close"]].min(axis=1) - dataframe["low"])
/ dataframe["close"]
)
# 价格与高低点的关系
dataframe["%-high_low_range"] = (
(dataframe["high"] - dataframe["low"]) / dataframe["close"]
)
return dataframe
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
"""
标准特征 - 不自动展开,只在基础时间框架上计算一次
适合放时间特征等不需要跨时间框架的特征
"""
# 时间特征(让模型学习时间规律)
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
dataframe["%-minute_of_hour"] = dataframe["date"].dt.minute
# 是否是高波动时段(美国开市等)
hour = dataframe["date"].dt.hour
dataframe["%-is_us_session"] = (
((hour >= 13) & (hour <= 21)) # UTC 13-21 = 美东 8am-4pm
).astype(int)
dataframe["%-is_asia_session"] = (
((hour >= 0) & (hour <= 8)) # UTC 0-8 = 亚洲时段
).astype(int)
# 连续涨跌统计
pct = dataframe["close"].pct_change()
dataframe["%-consec_up"] = (pct > 0).astype(int)
dataframe["%-consec_up"] = dataframe["%-consec_up"].groupby(
(dataframe["%-consec_up"] != dataframe["%-consec_up"].shift()).cumsum()
).cumcount() + 1
dataframe["%-consec_up"] = dataframe["%-consec_up"] * (pct > 0).astype(int)
dataframe["%-consec_down"] = (pct < 0).astype(int)
dataframe["%-consec_down"] = dataframe["%-consec_down"].groupby(
(dataframe["%-consec_down"] != dataframe["%-consec_down"].shift()).cumsum()
).cumcount() + 1
dataframe["%-consec_down"] = dataframe["%-consec_down"] * (pct < 0).astype(int)
# 近期波动率变化
dataframe["%-vol_change_5"] = (
dataframe["volume"].rolling(5).mean()
/ dataframe["volume"].rolling(20).mean()
)
# 价格距离近期高低点
dataframe["%-dist_high_20"] = (
dataframe["close"] / dataframe["high"].rolling(20).max() - 1
)
dataframe["%-dist_low_20"] = (
dataframe["close"] / dataframe["low"].rolling(20).min() - 1
)
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
"""
设置 AI 模型的预测目标
目标:预测未来 N 根K线的平均价格变化率
模型会学习:当前市场状态 → 未来价格走向
"""
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
# 回归目标:未来 N 根K线的平均收盘价相对当前的变化率
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-label_period)
.rolling(label_period)
.mean()
/ dataframe["close"]
- 1
)
return dataframe
# =====================================================
# 策略核心逻辑
# =====================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
populate_indicators 中调用 FreqAI
所有指标由 feature_engineering_*() 函数定义
"""
# FreqAI 会自动调用所有 feature_engineering_*() 函数
# 然后训练模型并返回预测结果
dataframe = self.freqai.start(dataframe, metadata, self)
# 计算动态阈值(z-score 方式)
# &-s_close 是模型预测的未来价格变化
# &-s_close_mean 和 &-s_close_std 是训练期间的统计值
# 当预测值超过 mean + factor * std 时,说明模型认为有较强的方向性
return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
"""
入场信号 - 基于 AI 预测
核心逻辑:
1. do_predict == 1:模型认为当前数据在训练分布内(可信)
2. &-s_close > threshold:预测未来上涨幅度超过阈值
3. 动态阈值 = mean + 1.0 * std(约 84% 置信度)
"""
# 动态阈值:使用训练期间的统计值
# 当 &-s_close_mean 和 &-s_close_std 可用时,用 z-score
# 否则用固定阈值
if "&-s_close_mean" in df.columns and "&-s_close_std" in df.columns:
long_threshold = df["&-s_close_mean"] + df["&-s_close_std"] * 1.0
short_threshold = df["&-s_close_mean"] - df["&-s_close_std"] * 1.0
else:
long_threshold = 0.005
short_threshold = -0.005
# 做多条件
enter_long_conditions = [
df["do_predict"] == 1, # 模型预测可信
df["&-s_close"] > long_threshold, # 预测超过动态阈值
]
if enter_long_conditions:
df.loc[
reduce(lambda x, y: x & y, enter_long_conditions),
["enter_long", "enter_tag"]
] = (1, "ai_long")
# 做空条件
enter_short_conditions = [
df["do_predict"] == 1, # 模型预测可信
df["&-s_close"] < short_threshold, # 预测低于动态阈值
]
if enter_short_conditions:
df.loc[
reduce(lambda x, y: x & y, enter_short_conditions),
["enter_short", "enter_tag"]
] = (1, "ai_short")
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
"""
出场信号 - AI 预测方向反转时退出
"""
# 多头退出:预测转为下跌
exit_long_conditions = [
df["do_predict"] == 1,
df["&-s_close"] < 0, # 预测未来下跌
]
if exit_long_conditions:
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
# 空头退出:预测转为上涨
exit_short_conditions = [
df["do_predict"] == 1,
df["&-s_close"] > 0, # 预测未来上涨
]
if exit_short_conditions:
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
return df
def confirm_trade_entry(
self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
side: str, **kwargs
) -> bool:
"""
实盘入场确认 - 防止滑点过大
"""
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = df.iloc[-1].squeeze()
if side == "long":
if rate > (last_candle["close"] * (1 + 0.0025)):
return False
else:
if rate < (last_candle["close"] * (1 - 0.0025)):
return False
return True