From 2e4b08ac8c39672ebc0e7bd7651c46a570607fc3 Mon Sep 17 00:00:00 2001 From: aszerW Date: Sun, 26 Oct 2025 01:52:05 +0800 Subject: [PATCH] =?UTF-8?q?ema=E5=8F=8C=E5=9D=87=E7=BA=BF=E8=B6=8B?= =?UTF-8?q?=E5=8A=BF=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trend.py | 81 ++++++++++ user_data/strategies/trend_strategy.py | 205 +++++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 trend.py create mode 100644 user_data/strategies/trend_strategy.py diff --git a/trend.py b/trend.py new file mode 100644 index 0000000..0bd4b53 --- /dev/null +++ b/trend.py @@ -0,0 +1,81 @@ +import vectorbt as vbt +import pandas as pd + +# ======================== +# 数据加载 +# ======================== +# 从本地读取 ETH/USDT 1天K线数据(来自OKX) +df = pd.read_feather("/Users/aszer/Documents/vscode/cta/user_data/data/okx/ADA_USDT-1d.feather") +# 提取收盘价作为交易价格 +price = df["close"] + +# ======================== +# 技术指标计算 +# ======================== +# 计算10日指数移动平均线(EMA) +ema_10 = vbt.MA.run(price, window=10, ewm=True).ma + +# 计算20日指数移动平均线(EMA) +ema_20 = vbt.MA.run(price, window=20, ewm=True).ma + +# 计算6周期相对强弱指数(RSI) +rsi_6 = vbt.RSI.run(price, window=6).rsi + +# 计算12周期相对强弱指数(RSI) +rsi_12 = vbt.RSI.run(price, window=12).rsi + +# ======================== +# 买入条件定义 +# ======================== +# 条件1:EMA多头排列 + 短期趋势加速 +# - EMA10 > EMA20:短期趋势强于长期趋势(多头排列) +# - EMA10昨日 > 前日:短期均线继续上行,显示动量增强 +ema_bullish_cross = (ema_10 > ema_20) & (ema_10.shift(1) > ema_10.shift(2)) + +# 条件2:双周期RSI进入深度超卖区,暗示反弹可能 +# - 过去3天(含前天)的6周期RSI最高值 < 21,表示近期极度超卖 +# - 过去3天的12周期RSI最高值 < 26.25,确认中周期也处于超卖状态 +# 注:此条件未显式检查“今日反弹”,可后续增强 +rsi_oversold_bounce = (rsi_6.shift(1).rolling(3).max() < 21) & \ + (rsi_12.shift(1).rolling(3).max() < 26.25) + +# 合并所有买入信号:任一条件满足即产生买入信号 +entries = ema_bullish_cross | rsi_oversold_bounce + +# ======================== +# 卖出条件定义 +# ======================== +# 计算长期均线与短期均线的价差(空头趋势强度) +diff = ema_20 - ema_10 # 当diff扩大,表示空头趋势加强 + +# 条件1:EMA10连续5天下跌(动量走弱) +# - 当前EMA10 < 过去5天(不含今日)的最低EMA10值 +ema10_falling = ema_10 < ema_10.shift(1).rolling(window=5).min() + +# 条件2:空头趋势加速(价差达到近期最大) +# - 当前diff是过去6天(含今日)中的最大值,表示空头力量最强 +diff_expanding = diff == diff.rolling(window=6).max() + +# 合并卖出信号:两个条件同时满足才卖出 +sell_cond = ema10_falling & diff_expanding +exits = sell_cond + +# ======================== +# 回测执行 +# ======================== +# 使用 vectorbt 的信号回测引擎,构建投资组合 +pf = vbt.Portfolio.from_signals( + price, # 价格序列 + entries, # 买入信号 + exits, # 卖出信号 + init_cash=100, # 初始资金 100 USDT + fees=0.001, # 交易手续费 0.1%(买卖均收) + sl_stop=0.05, # 止损:从最高价回撤5%时触发 + freq="1D" # 数据频率为每日,用于复利和年化计算 +) + +# ======================== +# 输出回测统计结果 +# ======================== +print(pf.stats()) + diff --git a/user_data/strategies/trend_strategy.py b/user_data/strategies/trend_strategy.py new file mode 100644 index 0000000..94bd856 --- /dev/null +++ b/user_data/strategies/trend_strategy.py @@ -0,0 +1,205 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 +# isort: skip_file +# --- Do not remove these libs --- +import numpy as np +import pandas as pd +from pandas import DataFrame +from datetime import datetime +from typing import Optional, Union + +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IntParameter, merge_informative_pair, stoploss_from_absolute, + stoploss_from_open) +from freqtrade.strategy.interface import IStrategy +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class TrendStrategy(IStrategy): + """ + 趋势策略 - 基于EMA和RSI的趋势跟踪策略 + + 策略逻辑: + 买入条件: + 1. EMA多头排列 + 短期趋势加速:EMA10 > EMA20 且 EMA10继续上行 + 2. 双周期RSI深度超卖反弹:6周期RSI和12周期RSI都进入超卖区域 + + 卖出条件: + 1. EMA10连续下跌(动量走弱) + 2. 空头趋势加速(EMA价差达到近期最大) + """ + + # Strategy interface version - allow new iterations of the strategy + INTERFACE_VERSION = 3 + + # Optimal timeframe for the strategy + timeframe = '1d' + + # Can this strategy go short? + can_short: bool = False + + # Minimal ROI designed for the strategy. + # minimal_roi = { + # "0": 0.1 # 10% ROI,让止损和卖出条件来控制退出 + # } + + # Optimal stoploss designed for the strategy + stoploss = -0.05 # 5% 止损 + + # Trailing stoploss + trailing_stop = False + trailing_stop_positive = None + trailing_stop_positive_offset = 0.0 + trailing_only_offset_is_reached = False + + # Run "populate_indicators" only for new candle + process_only_new_candles = False + + # These values can be overridden in the config + use_exit_signal = True + exit_profit_only = False + ignore_roi_if_entry_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 30 + + # Optional order type mapping + order_types = { + 'entry': 'limit', + 'exit': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force + order_time_in_force = { + 'entry': 'gtc', + 'exit': 'gtc' + } + + # 策略参数 + ema_short_window = IntParameter(8, 15, default=10, space="buy") + ema_long_window = IntParameter(15, 25, default=20, space="buy") + rsi_short_window = IntParameter(4, 8, default=6, space="buy") + rsi_long_window = IntParameter(10, 15, default=12, space="buy") + + # RSI超卖阈值 + rsi_short_oversold = DecimalParameter(15.0, 25.0, default=21.0, space="buy") + rsi_long_oversold = DecimalParameter(20.0, 30.0, default=26.25, space="buy") + + # 趋势检查窗口 + trend_check_window = IntParameter(3, 7, default=3, space="buy") + momentum_check_window = IntParameter(3, 7, default=5, space="sell") + diff_check_window = IntParameter(4, 8, default=6, space="sell") + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pairs will automatically be available for use in the `populate_indicators` method. + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategy + """ + + # EMA指标 + dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=self.ema_short_window.value) + dataframe['ema_long'] = ta.EMA(dataframe, timeperiod=self.ema_long_window.value) + + # RSI指标 + dataframe['rsi_short'] = ta.RSI(dataframe, timeperiod=self.rsi_short_window.value) + dataframe['rsi_long'] = ta.RSI(dataframe, timeperiod=self.rsi_long_window.value) + + # 计算EMA价差(用于卖出条件) + dataframe['ema_diff'] = dataframe['ema_long'] - dataframe['ema_short'] + + # 计算EMA短期趋势(用于买入条件) + dataframe['ema_short_rising'] = dataframe['ema_short'] > dataframe['ema_short'].shift(1) + dataframe['ema_short_rising_2'] = dataframe['ema_short'].shift(1) > dataframe['ema_short'].shift(2) + + # 计算RSI超卖条件 + dataframe['rsi_short_oversold'] = ( + dataframe['rsi_short'].shift(1).rolling(window=self.trend_check_window.value).max() < self.rsi_short_oversold.value + ) + dataframe['rsi_long_oversold'] = ( + dataframe['rsi_long'].shift(1).rolling(window=self.trend_check_window.value).max() < self.rsi_long_oversold.value + ) + + # 计算EMA10连续下跌条件(用于卖出) + dataframe['ema_short_falling'] = ( + dataframe['ema_short'] < dataframe['ema_short'].shift(1).rolling(window=self.momentum_check_window.value).min() + ) + + # 计算价差扩大条件(用于卖出) + dataframe['ema_diff_expanding'] = ( + dataframe['ema_diff'] == dataframe['ema_diff'].rolling(window=self.diff_check_window.value).max() + ) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the entry signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with entry columns populated + """ + + # 条件1:EMA多头排列 + 短期趋势加速 + ema_bullish_cross = ( + (dataframe['ema_short'] > dataframe['ema_long']) & + (dataframe['ema_short_rising'] & dataframe['ema_short_rising_2']) + ) + + # 条件2:双周期RSI深度超卖反弹 + rsi_oversold_bounce = ( + dataframe['rsi_short_oversold'] & + dataframe['rsi_long_oversold'] + ) + + # 合并买入条件:任一条件满足即产生买入信号 + dataframe.loc[ + ema_bullish_cross | rsi_oversold_bounce, + 'enter_long' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the exit signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with exit columns populated + """ + + # 卖出条件:EMA10连续下跌 + 空头趋势加速 + sell_condition = ( + dataframe['ema_short_falling'] & + dataframe['ema_diff_expanding'] + ) + + dataframe.loc[ + sell_condition, + 'exit_long' + ] = 1 + + return dataframe + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + """ + 自定义止损逻辑 + 这里保持原有的5%止损,但可以根据需要添加更复杂的止损逻辑 + """ + return self.stoploss