hyperopt ok
This commit is contained in:
parent
894b6c5f50
commit
b1c6720c93
@ -19,6 +19,8 @@
|
||||
"order_book_top": 1
|
||||
},
|
||||
"stake_currency": "USDT",
|
||||
"trailing_stop": false,
|
||||
"use_custom_stoploss": true,
|
||||
"startup_candle_count": 200,
|
||||
"tradable_balance_ratio": 1,
|
||||
"fiat_display_currency": "USD",
|
||||
@ -27,7 +29,23 @@
|
||||
"timeframe": "5m",
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": true,
|
||||
"stoploss": -0.1,
|
||||
"stoploss": -0.061,
|
||||
"roi": {},
|
||||
"trailing": {
|
||||
"trailing_stop": false,
|
||||
"trailing_stop_positive": null,
|
||||
"trailing_stop_positive_offset": 0.0,
|
||||
"trailing_only_offset_is_reached": false
|
||||
},
|
||||
"max_open_trades": 3,
|
||||
"buy": {
|
||||
"adx_buy": 25,
|
||||
"atr_ratio": 0.005
|
||||
},
|
||||
"sell": {
|
||||
"ema_fast_period": 7,
|
||||
"rsi_sell": 60
|
||||
},
|
||||
"exchange": {
|
||||
"name": "okx",
|
||||
"key": "REDACTED",
|
||||
@ -45,7 +63,7 @@
|
||||
"rateLimit": 3000,
|
||||
"timeout": 20000
|
||||
},
|
||||
"pair_whitelist": ["OKB/USDT", "BTC/USDT", "ETH/USDT", "SOL/USDT", "DOT/USDT", "TON/USDT"],
|
||||
"pair_whitelist": ["BTC/USDT", "ETH/USDT", "SOL/USDT", "DOT/USDT"],
|
||||
"pair_blacklist": []
|
||||
},
|
||||
"pairlists": [
|
||||
|
||||
BIN
doc/.hyperopts.md.swp
Normal file
BIN
doc/.hyperopts.md.swp
Normal file
Binary file not shown.
14
doc/hyperopts.md
Normal file
14
doc/hyperopts.md
Normal file
@ -0,0 +1,14 @@
|
||||
## hyperopts如何使用
|
||||
|
||||
每周运行一次 hyperopt, 每次运行最近2个月的数据, 得到 stoploss 最优解后 手动更新 config, 提交代码
|
||||
|
||||
假设今天是2025年5月15日
|
||||
```
|
||||
cd tools
|
||||
./download.sh
|
||||
./hyperopt.sh 20250314 20250516
|
||||
然后得到结果, stoploss 为 -0.06 写到log里
|
||||
```
|
||||
|
||||
后面改成自动更新并不麻烦
|
||||
策略目录有自动生成的json, 是hypertop计算结果, 需脱掉一层 paramas后 方可使用
|
||||
@ -1,59 +0,0 @@
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||
from pandas import DataFrame
|
||||
import numpy as np
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AIEnhancedStrategy(IStrategy):
|
||||
INTERFACE_VERSION = 3
|
||||
|
||||
can_short = False # 只做多
|
||||
timeframe = '5m'
|
||||
|
||||
process_only_new_candles = True
|
||||
use_exit_signal = True
|
||||
exit_profit_only = False
|
||||
ignore_roi_if_entry_signal = False
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
|
||||
metadata: dict, **kwargs) -> DataFrame:
|
||||
# 自定义特征工程
|
||||
for col in ['open', 'high', 'low', 'close', 'volume']:
|
||||
dataframe[f'{col}_pct_change'] = dataframe[col].pct_change(periods=period)
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
# 目标变量:未来n根K线的收盘价变化
|
||||
dataframe['&s-close_pct'] = dataframe['close'].pct_change(periods=5).shift(-5)
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dk = self.freqai_info.get("dk", None)
|
||||
if not dk:
|
||||
raise ValueError("FreqaiDataKitchen is not available.")
|
||||
|
||||
dataframe = dk.feature_engineering_standard(dataframe, metadata, self)
|
||||
|
||||
if self.config["runmode"].value in ("live", "dry_run"):
|
||||
dataframe = dk.live_models(self, dataframe, metadata=metadata)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 预测值大于阈值时开多仓
|
||||
dataframe.loc[
|
||||
(dataframe["&s-close_pct"] > 0.002),
|
||||
"enter_long"
|
||||
] = 1
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 预测值小于负阈值时平仓
|
||||
dataframe.loc[
|
||||
(dataframe["&s-close_pct"] < -0.001),
|
||||
"exit_long"
|
||||
] = 1
|
||||
return dataframe
|
||||
|
||||
@ -1,190 +0,0 @@
|
||||
from freqtrade.strategy import IStrategy, IntParameter, RealParameter, DecimalParameter, BooleanParameter
|
||||
from pandas import DataFrame
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from datetime import datetime
|
||||
|
||||
class BsseHyperOptStrategy(IStrategy):
|
||||
# 策略的基本配置
|
||||
timeframe = '5m'
|
||||
minimal_roi = {"40": 0.0, "30": 0.01, "20": 0.02, "0": 0.04}
|
||||
stoploss = -0.10
|
||||
startup_candle_count = 20
|
||||
|
||||
# 可选的订单类型映射
|
||||
order_types = {
|
||||
"entry": "limit",
|
||||
"exit": "limit",
|
||||
"stoploss": "limit",
|
||||
"stoploss_on_exchange": False,
|
||||
}
|
||||
|
||||
# 可选的订单有效时间
|
||||
order_time_in_force = {
|
||||
"entry": "gtc",
|
||||
"exit": "gtc",
|
||||
}
|
||||
|
||||
# 可优化的参数
|
||||
buy_rsi = IntParameter(30, 50, default=35, space='buy')
|
||||
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
|
||||
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
|
||||
sell_minusdi = DecimalParameter(
|
||||
low=0, high=1, default=0.5001, decimals=3, space='sell', load=False
|
||||
)
|
||||
protection_enabled = BooleanParameter(default=True)
|
||||
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||
|
||||
@property
|
||||
def protections(self):
|
||||
prot = []
|
||||
if self.protection_enabled.value:
|
||||
prot.append(
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": self.protection_cooldown_lookback.value,
|
||||
}
|
||||
)
|
||||
return prot
|
||||
|
||||
def bot_start(self):
|
||||
pass
|
||||
|
||||
def informative_pairs(self):
|
||||
return []
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 动量指标
|
||||
# ADX
|
||||
dataframe["adx"] = ta.ADX(dataframe)
|
||||
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe["macd"] = macd["macd"]
|
||||
dataframe["macdsignal"] = macd["macdsignal"]
|
||||
dataframe["macdhist"] = macd["macdhist"]
|
||||
|
||||
# 负向指标
|
||||
dataframe["minus_di"] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# 正向指标
|
||||
dataframe["plus_di"] = ta.PLUS_DI(dataframe)
|
||||
|
||||
# RSI
|
||||
dataframe["rsi"] = ta.RSI(dataframe)
|
||||
|
||||
# 快速随机指标
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe["fastd"] = stoch_fast["fastd"]
|
||||
dataframe["fastk"] = stoch_fast["fastk"]
|
||||
|
||||
# 布林带
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
|
||||
# 指数移动平均线
|
||||
dataframe["ema10"] = ta.EMA(dataframe, timeperiod=10)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe["rsi"] < self.buy_rsi.value)
|
||||
& (dataframe["fastd"] < 35)
|
||||
& (dataframe["adx"] > 30)
|
||||
& (dataframe["plus_di"] > self.buy_plusdi.value)
|
||||
)
|
||||
| ((dataframe["adx"] > 65) & (dataframe["plus_di"] > self.buy_plusdi.value)),
|
||||
"enter_long",
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
(qtpylib.crossed_above(dataframe["rsi"], self.sell_rsi.value))
|
||||
| (qtpylib.crossed_above(dataframe["fastd"], 70))
|
||||
)
|
||||
& (dataframe["adx"] > 10)
|
||||
& (dataframe["minus_di"] > 0)
|
||||
)
|
||||
| ((dataframe["adx"] > 70) & (dataframe["minus_di"] > self.sell_minusdi.value)),
|
||||
"exit_long",
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def leverage(
|
||||
self,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
current_rate: float,
|
||||
proposed_leverage: float,
|
||||
max_leverage: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
return 3.0
|
||||
|
||||
def adjust_trade_position(
|
||||
self,
|
||||
trade,
|
||||
current_time: datetime,
|
||||
current_rate: float,
|
||||
current_profit: float,
|
||||
min_stake: float | None,
|
||||
max_stake: float,
|
||||
current_entry_rate: float,
|
||||
current_exit_rate: float,
|
||||
current_entry_profit: float,
|
||||
current_exit_profit: float,
|
||||
**kwargs,
|
||||
) -> float | None:
|
||||
if current_profit < -0.0075:
|
||||
orders = trade.select_filled_orders(trade.entry_side)
|
||||
return round(orders[0].stake_amount, 0)
|
||||
return None
|
||||
|
||||
class HyperOpt:
|
||||
def buy_indicator_space(self):
|
||||
return [
|
||||
self.strategy.buy_rsi.get_space('buy_rsi'),
|
||||
self.strategy.buy_plusdi.get_space('buy_plusdi')
|
||||
]
|
||||
|
||||
def sell_indicator_space(self):
|
||||
return [
|
||||
self.strategy.sell_rsi.get_space('sell_rsi'),
|
||||
self.strategy.sell_minusdi.get_space('sell_minusdi')
|
||||
]
|
||||
|
||||
def roi_space(self):
|
||||
from skopt.space import Integer
|
||||
return [
|
||||
Integer(10, 120, name='roi_t1'),
|
||||
Integer(10, 60, name='roi_t2'),
|
||||
Integer(10, 40, name='roi_t3'),
|
||||
]
|
||||
|
||||
def stoploss_space(self):
|
||||
from skopt.space import Real
|
||||
return [Real(-0.5, -0.01, name='stoploss')]
|
||||
|
||||
def trailing_space(self):
|
||||
from skopt.space import Real, Categorical
|
||||
return [
|
||||
Categorical([True, False], name='trailing_stop'),
|
||||
Real(0.01, 0.1, name='trailing_stop_positive'),
|
||||
Real(0.0, 0.1, name='trailing_stop_positive_offset'),
|
||||
Real(0.0, 0.1, name='trailing_only_offset_is_reached')
|
||||
]
|
||||
|
||||
def max_open_trades_space(self):
|
||||
from skopt.space import Integer
|
||||
return [Integer(1, 10, name='max_open_trades')]``
|
||||
@ -1,297 +0,0 @@
|
||||
import logging
|
||||
|
||||
import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical import qtpylib
|
||||
|
||||
from freqtrade.strategy import IntParameter, IStrategy, merge_informative_pair
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FreqaiExampleHybridStrategy(IStrategy):
|
||||
"""
|
||||
Example of a hybrid FreqAI strat, designed to illustrate how a user may employ
|
||||
FreqAI to bolster a typical Freqtrade strategy.
|
||||
|
||||
Launching this strategy would be:
|
||||
|
||||
freqtrade trade --strategy FreqaiExampleHybridStrategy --strategy-path freqtrade/templates
|
||||
--freqaimodel CatboostClassifier --config config_examples/config_freqai.example.json
|
||||
|
||||
or the user simply adds this to their config:
|
||||
|
||||
"freqai": {
|
||||
"enabled": true,
|
||||
"purge_old_models": 2,
|
||||
"train_period_days": 15,
|
||||
"identifier": "unique-id",
|
||||
"feature_parameters": {
|
||||
"include_timeframes": [
|
||||
"3m",
|
||||
"15m",
|
||||
"1h"
|
||||
],
|
||||
"include_corr_pairlist": [
|
||||
"BTC/USDT",
|
||||
"ETH/USDT"
|
||||
],
|
||||
"label_period_candles": 20,
|
||||
"include_shifted_candles": 2,
|
||||
"DI_threshold": 0.9,
|
||||
"weight_factor": 0.9,
|
||||
"principal_component_analysis": false,
|
||||
"use_SVM_to_remove_outliers": true,
|
||||
"indicator_periods_candles": [10, 20]
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0,
|
||||
"random_state": 1
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 200,
|
||||
"max_depth": 5,
|
||||
"learning_rate": 0.05
|
||||
}
|
||||
},
|
||||
|
||||
Thanks to @smarmau and @johanvulgt for developing and sharing the strategy.
|
||||
"""
|
||||
|
||||
minimal_roi = {
|
||||
# "120": 0.0, # exit after 120 minutes at break even
|
||||
"60": 0.01,
|
||||
"30": 0.02,
|
||||
"0": 0.04,
|
||||
}
|
||||
|
||||
plot_config = {
|
||||
"main_plot": {
|
||||
"tema": {},
|
||||
},
|
||||
"subplots": {
|
||||
"MACD": {
|
||||
"macd": {"color": "blue"},
|
||||
"macdsignal": {"color": "orange"},
|
||||
},
|
||||
"RSI": {
|
||||
"rsi": {"color": "red"},
|
||||
},
|
||||
"Up_or_down": {
|
||||
"&s-up_or_down": {"color": "green"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
process_only_new_candles = True
|
||||
stoploss = -0.05
|
||||
use_exit_signal = True
|
||||
startup_candle_count: int = 30
|
||||
can_short = False
|
||||
|
||||
# Hyperoptable parameters
|
||||
buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True)
|
||||
|
||||
def feature_engineering_expand_all(
|
||||
self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
|
||||
) -> DataFrame:
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
|
||||
`include_corr_pairs`. In other words, a single feature defined in this function
|
||||
will automatically expand to a total of
|
||||
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
|
||||
`include_corr_pairs` numbers of features added to the model.
|
||||
|
||||
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||
|
||||
More details on how these config defined parameters accelerate feature engineering
|
||||
in the documentation at:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
||||
|
||||
:param dataframe: strategy dataframe which will receive the features
|
||||
:param period: period of the indicator - usage example:
|
||||
:param metadata: metadata of current pair
|
||||
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||
"""
|
||||
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
|
||||
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||
|
||||
bollinger = qtpylib.bollinger_bands(
|
||||
qtpylib.typical_price(dataframe), window=period, stds=2
|
||||
)
|
||||
dataframe["bb_lowerband-period"] = bollinger["lower"]
|
||||
dataframe["bb_middleband-period"] = bollinger["mid"]
|
||||
dataframe["bb_upperband-period"] = bollinger["upper"]
|
||||
|
||||
dataframe["%-bb_width-period"] = (
|
||||
dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]
|
||||
) / dataframe["bb_middleband-period"]
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_expand_basic(
|
||||
self, dataframe: DataFrame, metadata: dict, **kwargs
|
||||
) -> DataFrame:
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This function will automatically expand the defined features on the config defined
|
||||
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||
In other words, a single feature defined in this function
|
||||
will automatically expand to a total of
|
||||
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
|
||||
numbers of features added to the model.
|
||||
|
||||
Features defined here will *not* be automatically duplicated on user defined
|
||||
`indicator_periods_candles`
|
||||
|
||||
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||
|
||||
More details on how these config defined parameters accelerate feature engineering
|
||||
in the documentation at:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
||||
|
||||
:param dataframe: strategy dataframe which will receive the features
|
||||
:param metadata: metadata of current pair
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
|
||||
"""
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(
|
||||
self, dataframe: DataFrame, metadata: dict, **kwargs
|
||||
) -> DataFrame:
|
||||
"""
|
||||
*Only functional with FreqAI enabled strategies*
|
||||
This optional function will be called once with the dataframe of the base timeframe.
|
||||
This is the final function to be called, which means that the dataframe entering this
|
||||
function will contain all the features and columns created by all other
|
||||
freqai_feature_engineering_* functions.
|
||||
|
||||
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
|
||||
This function is a good place for any feature that should not be auto-expanded upon
|
||||
(e.g. day of the week).
|
||||
|
||||
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||
|
||||
More details about feature engineering available:
|
||||
|
||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
||||
|
||||
:param dataframe: strategy dataframe which will receive the features
|
||||
:param metadata: metadata of current pair
|
||||
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||
"""
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
return dataframe
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
"""
|
||||
Redefined target variable to predict whether the price will increase or decrease in the future.
|
||||
"""
|
||||
logger.info(f"Setting FreqAI targets for pair: {metadata['pair']}")
|
||||
|
||||
if "close" not in dataframe.columns:
|
||||
logger.error("Required 'close' column missing in dataframe")
|
||||
raise ValueError("Required 'close' column missing in dataframe")
|
||||
|
||||
if len(dataframe) < 50:
|
||||
logger.error(f"Insufficient data: {len(dataframe)} rows, need at least 50 for shift(-50)")
|
||||
raise ValueError("Insufficient data for target calculation")
|
||||
|
||||
try:
|
||||
# Define target variable: 1 for price increase, 0 for price decrease
|
||||
dataframe["&-up_or_down"] = np.where(
|
||||
dataframe["close"].shift(-50) > dataframe["close"], 1, 0
|
||||
)
|
||||
# Ensure target variable is a 2D array
|
||||
dataframe["&-up_or_down"] = dataframe["&-up_or_down"].values.reshape(-1, 1)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create &-up_or_down column: {str(e)}")
|
||||
raise
|
||||
|
||||
logger.info("FreqAI targets set successfully")
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.info(f"Processing pair: {metadata['pair']}")
|
||||
logger.info(f"Input DataFrame shape: {dataframe.shape}")
|
||||
logger.info(f"Input DataFrame columns: {list(dataframe.columns)}")
|
||||
logger.info(f"Input DataFrame head:\n{dataframe[['date', 'close', 'volume']].head().to_string()}")
|
||||
|
||||
# Ensure FreqAI processing
|
||||
logger.info("Calling self.freqai.start")
|
||||
try:
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
except Exception as e:
|
||||
logger.error(f"self.freqai.start failed: {str(e)}")
|
||||
raise
|
||||
logger.info("self.freqai.start completed")
|
||||
|
||||
logger.info(f"Output DataFrame shape: {dataframe.shape}")
|
||||
logger.info(f"Output DataFrame columns: {list(dataframe.columns)}")
|
||||
# Safely log columns that exist
|
||||
available_columns = [col for col in ['date', 'close', '&-up_or_down'] if col in dataframe.columns]
|
||||
logger.info(f"Output DataFrame head:\n{dataframe[available_columns].head().to_string()}")
|
||||
|
||||
if "&-up_or_down" not in dataframe.columns:
|
||||
logger.error("FreqAI did not generate the required &-up_or_down column")
|
||||
raise KeyError("FreqAI did not generate the required &-up_or_down column")
|
||||
|
||||
# RSI
|
||||
dataframe["rsi"] = ta.RSI(dataframe)
|
||||
|
||||
# Bollinger Bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
dataframe["bb_percent"] = (dataframe["close"] - dataframe["bb_lowerband"]) / (
|
||||
dataframe["bb_upperband"] - dataframe["bb_lowerband"]
|
||||
)
|
||||
dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe[
|
||||
"bb_middleband"
|
||||
]
|
||||
|
||||
# TEMA
|
||||
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
return dataframe
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
df.loc[
|
||||
(
|
||||
(qtpylib.crossed_above(df["rsi"], self.buy_rsi.value))
|
||||
& (df["tema"] <= df["bb_middleband"])
|
||||
& (df["tema"] > df["tema"].shift(1))
|
||||
& (df["volume"] > 0)
|
||||
),
|
||||
"enter_long",
|
||||
] = 1
|
||||
return df
|
||||
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
df.loc[
|
||||
(
|
||||
(qtpylib.crossed_above(df["rsi"], self.sell_rsi.value))
|
||||
& (df["tema"] > df["bb_middleband"])
|
||||
& (df["tema"] < df["tema"].shift(1))
|
||||
& (df["volume"] > 0)
|
||||
),
|
||||
"exit_long",
|
||||
] = 1
|
||||
return df
|
||||
@ -1,32 +0,0 @@
|
||||
{
|
||||
"strategy_name": "FreqaiExampleStrategy",
|
||||
"params": {
|
||||
"trailing": {
|
||||
"trailing_stop": true,
|
||||
"trailing_stop_positive": 0.01,
|
||||
"trailing_stop_positive_offset": 0.02,
|
||||
"trailing_only_offset_is_reached": false
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 4
|
||||
},
|
||||
"buy": {
|
||||
"buy_rsi": 39.92672300850069
|
||||
},
|
||||
"sell": {
|
||||
"sell_rsi": 69.92672300850067
|
||||
},
|
||||
"protection": {},
|
||||
"roi": {
|
||||
"0": 0.132,
|
||||
"8": 0.047,
|
||||
"14": 0.007,
|
||||
"60": 0
|
||||
},
|
||||
"stoploss": {
|
||||
"stoploss": -0.322
|
||||
}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-04-23 12:30:05.550433+00:00"
|
||||
}
|
||||
@ -1,395 +0,0 @@
|
||||
import logging
|
||||
import numpy as np
|
||||
import pandas as pd # 添加 pandas 导入
|
||||
from functools import reduce
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from typing import Dict, List, Optional
|
||||
from technical import qtpylib
|
||||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FreqaiExampleStrategy(IStrategy):
|
||||
# 移除硬编码的 minimal_roi 和 stoploss,改为动态适配
|
||||
minimal_roi = {} # 将在 populate_indicators 中动态生成
|
||||
stoploss = 0.0 # 将在 populate_indicators 中动态设置
|
||||
trailing_stop = True
|
||||
process_only_new_candles = True
|
||||
use_exit_signal = True
|
||||
startup_candle_count: int = 40
|
||||
can_short = False
|
||||
|
||||
# 参数定义:FreqAI 动态适配 buy_rsi 和 sell_rsi,禁用 Hyperopt 优化
|
||||
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
|
||||
|
||||
# 为 Hyperopt 优化添加 ROI 和 stoploss 参数
|
||||
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, load=True)
|
||||
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.027, space="roi", optimize=True, load=True)
|
||||
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.009, space="roi", optimize=True, load=True)
|
||||
stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.182, space="stoploss", optimize=True, load=True)
|
||||
|
||||
# FreqAI 配置
|
||||
freqai_info = {
|
||||
"model": "XGBoostRegressor", # 与config保持一致
|
||||
"save_backtest_models": True,
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["3m", "15m", "1h"], # 与config一致:w
|
||||
"include_corr_pairlist": ["BTC/USDT", "SOL/USDT"], # 添加相关交易对
|
||||
"label_period_candles": 20, # 与config一致
|
||||
"include_shifted_candles": 2, # 与config一致
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.2,
|
||||
"shuffle": True, # 启用shuffle
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 100, # 减少树的数量
|
||||
"learning_rate": 0.1, # 提高学习率
|
||||
"max_depth": 6, # 限制树深度
|
||||
"subsample": 0.8, # 添加子采样
|
||||
"colsample_bytree": 0.8, # 添加特征采样
|
||||
"objective": "reg:squarederror",
|
||||
"eval_metric": "rmse",
|
||||
"early_stopping_rounds": 20,
|
||||
"verbose": 0,
|
||||
},
|
||||
"data_kitchen": {
|
||||
"feature_parameters": {
|
||||
"DI_threshold": 1.5, # 降低异常值过滤阈值
|
||||
"use_DBSCAN_to_remove_outliers": False # 禁用DBSCAN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plot_config = {
|
||||
"main_plot": {},
|
||||
"subplots": {
|
||||
"&-buy_rsi": {"&-buy_rsi": {"color": "green"}},
|
||||
"&-sell_rsi": {"&-sell_rsi": {"color": "red"}},
|
||||
"&-stoploss": {"&-stoploss": {"color": "purple"}},
|
||||
"&-roi_0": {"&-roi_0": {"color": "orange"}},
|
||||
"do_predict": {"do_predict": {"color": "brown"}},
|
||||
},
|
||||
}
|
||||
def __init__(self, config: Dict):
|
||||
super().__init__(config)
|
||||
# 初始化特征缓存
|
||||
self.feature_cache = {}
|
||||
# 设置日志级别
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 输出模型路径用于调试
|
||||
freqai_model_path = self.config.get("freqai", {}).get("model_path", "/freqtrade/user_data/models")
|
||||
logger.info(f"FreqAI 模型路径:{freqai_model_path}")
|
||||
|
||||
|
||||
def _normalize_column(self, series: pd.Series) -> pd.Series:
|
||||
"""对单个列进行最小最大归一化"""
|
||||
if series.nunique() <= 1:
|
||||
# 如果列中所有值都相同或为空,直接返回全0
|
||||
return pd.Series(np.zeros_like(series), index=series.index)
|
||||
|
||||
min_val = series.min()
|
||||
max_val = series.max()
|
||||
normalized = (series - min_val) / (max_val - min_val)
|
||||
return normalized.fillna(0)
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
|
||||
# 基础指标
|
||||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
dataframe["macd"], dataframe["macdsignal"], dataframe["macdhist"] = ta.MACD(
|
||||
dataframe, fastperiod=12, slowperiod=26, signalperiod=9
|
||||
)
|
||||
|
||||
# 布林带及其宽度
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"]
|
||||
|
||||
# ATR (Average True Range)
|
||||
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
|
||||
|
||||
# RSI 变化率
|
||||
dataframe["rsi_gradient"] = dataframe["rsi"].diff().fillna(0)
|
||||
|
||||
# 数据清理与归一化
|
||||
for col in dataframe.select_dtypes(include=[np.number]).columns:
|
||||
# Ensure column is valid and contains more than one unique value to avoid division by zero
|
||||
if dataframe[col].nunique() > 1:
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe[col] = dataframe[col].ffill().fillna(0)
|
||||
dataframe[f"{col}_norm"] = self._normalize_column(dataframe[col])
|
||||
else:
|
||||
dataframe[f"{col}_norm"] = 0 # Default if normalization not possible
|
||||
logger.info(f"特征工程完成,特征数量:{len(dataframe.columns)}")
|
||||
return dataframe
|
||||
|
||||
def _add_noise(self, dataframe: DataFrame, noise_level: float = 0.02) -> DataFrame:
|
||||
"""为数值型特征添加随机噪声以增强模型泛化能力"""
|
||||
df = dataframe.copy()
|
||||
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
|
||||
for col in numeric_cols:
|
||||
noise = np.random.normal(loc=0.0, scale=noise_level * df[col].std(), size=df.shape[0])
|
||||
df[col] += noise
|
||||
logger.info(f"已向 {len(numeric_cols)} 个数值型特征添加 {noise_level * 100:.0f}% 噪声")
|
||||
return df
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
dataframe["%-volume_change"] = dataframe["volume"].pct_change(periods=5)
|
||||
dataframe["%-price_momentum"] = dataframe["close"] / dataframe["close"].shift(20) - 1
|
||||
|
||||
# 数据清理逻辑
|
||||
for col in dataframe.columns:
|
||||
if dataframe[col].dtype in ["float64", "int64"]:
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
|
||||
dataframe[col] = dataframe[col].ffill()
|
||||
dataframe[col] = dataframe[col].fillna(0)
|
||||
|
||||
# 检查是否仍有无效值
|
||||
if dataframe[col].isna().any() or np.isinf(dataframe[col]).any():
|
||||
logger.warning(f"列 {col} 仍包含无效值,已填充为默认值")
|
||||
dataframe[col] = dataframe[col].fillna(0)
|
||||
# 添加 2% 噪声以增强模型鲁棒性
|
||||
dataframe = self._add_noise(dataframe, noise_level=0.02)
|
||||
|
||||
logger.info(f"特征工程完成,特征数量:{len(dataframe.columns)}")
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||
dataframe.replace([np.inf, -np.inf], 0, inplace=True)
|
||||
dataframe.ffill(inplace=True)
|
||||
dataframe.fillna(0, inplace=True)
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
|
||||
if "close" not in dataframe.columns:
|
||||
logger.error("数据框缺少必要的 'close' 列")
|
||||
raise ValueError("数据框缺少必要的 'close' 列")
|
||||
|
||||
try:
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
|
||||
# 定义目标变量为未来价格变化百分比(连续值)
|
||||
dataframe["up_or_down"] = (
|
||||
dataframe["close"].shift(-label_period) - dataframe["close"]
|
||||
) / dataframe["close"]
|
||||
|
||||
# 数据清理:处理 NaN 和 Inf 值
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
|
||||
|
||||
# 确保目标变量是二维数组
|
||||
if dataframe["up_or_down"].ndim == 1:
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].values.reshape(-1, 1)
|
||||
|
||||
# 检查并处理 NaN 或无限值
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
|
||||
|
||||
# 生成 %-volatility 特征
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||||
|
||||
# 确保 &-buy_rsi_pred 列的值计算正确
|
||||
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
if "&-buy_rsi_pred" not in dataframe.columns:
|
||||
logger.warning("&-buy_rsi_pred 列不存在,正在使用 &-buy_rsi 模拟替代")
|
||||
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(window=10).mean().clip(20, 40)
|
||||
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi_pred"].fillna(dataframe["&-buy_rsi_pred"].median())
|
||||
|
||||
# 确保 &-sell_rsi_pred 存在(基于 buy_rsi_pred + 偏移量)
|
||||
if "&-sell_rsi_pred" not in dataframe.columns:
|
||||
logger.warning("&-sell_rsi_pred 列不存在,正在使用 &-buy_rsi_pred + 20 模拟替代")
|
||||
dataframe["&-sell_rsi_pred"] = dataframe["&-buy_rsi_pred"] + 20
|
||||
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].clip(50, 90)
|
||||
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].fillna(dataframe["&-sell_rsi_pred"].median())
|
||||
|
||||
# 数据清理
|
||||
for col in ["&-buy_rsi", "up_or_down", "%-volatility"]:
|
||||
# 使用直接操作避免链式赋值
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe[col] = dataframe[col].ffill() # 替代 fillna(method='ffill')
|
||||
dataframe[col] = dataframe[col].fillna(dataframe[col].mean()) # 使用均值填充 NaN 值
|
||||
if dataframe[col].isna().any():
|
||||
logger.warning(f"目标列 {col} 仍包含 NaN,填充为默认值")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||||
raise
|
||||
|
||||
# Log the shape of the target variable for debugging
|
||||
logger.info(f"目标列形状:{dataframe['up_or_down'].shape}")
|
||||
logger.info(f"目标列预览:\n{dataframe[['up_or_down', '&-buy_rsi']].head().to_string()}")
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.info(f"处理交易对:{metadata['pair']}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
# 计算传统指标
|
||||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# 生成 up_or_down 信号(非 FreqAI 目标)
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
dataframe["up_or_down"] = np.where(
|
||||
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
|
||||
)
|
||||
|
||||
# 确保 do_predict 列存在并填充默认值
|
||||
if "do_predict" not in dataframe.columns:
|
||||
dataframe["do_predict"] = 0
|
||||
logger.warning("do_predict 列不存在,已创建并填充为 0。")
|
||||
|
||||
# 添加详细日志以验证 do_predict 的值
|
||||
if "do_predict" in dataframe.columns:
|
||||
logger.debug(f"do_predict 列存在,前5行预览:\n{dataframe[['do_predict']].head().to_string()}")
|
||||
else:
|
||||
logger.warning("do_predict 列不存在,可能未正确加载模型或进行预测。")
|
||||
|
||||
# 确保 &-buy_rsi_pred 存在(如果模型未提供则模拟生成)
|
||||
if "&-buy_rsi" in dataframe.columns:
|
||||
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(window=10).mean().clip(20, 40)
|
||||
dataframe["&-buy_rsi_pred"] = dataframe["&-buy_rsi_pred"].fillna(dataframe["&-buy_rsi_pred"].median())
|
||||
else:
|
||||
logger.warning("&-buy_rsi 列不存在,无法生成 &-buy_rsi_pred,将使用默认值")
|
||||
dataframe["&-buy_rsi_pred"] = 27 # 默认 RSI 买入阈值
|
||||
|
||||
# 生成 &-sell_rsi_pred(基于 buy_rsi_pred + 偏移量)
|
||||
if "&-buy_rsi_pred" in dataframe.columns:
|
||||
dataframe["&-sell_rsi_pred"] = dataframe["&-buy_rsi_pred"] + 20
|
||||
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].clip(50, 90)
|
||||
dataframe["&-sell_rsi_pred"] = dataframe["&-sell_rsi_pred"].fillna(dataframe["&-sell_rsi_pred"].median())
|
||||
else:
|
||||
logger.warning("&-buy_rsi_pred 列不存在,无法生成 &-sell_rsi_pred,将使用默认值")
|
||||
dataframe["&-sell_rsi_pred"] = 59 # 默认 RSI 卖出阈值
|
||||
|
||||
# 确保 &-sell_rsi_pred 最终存在于 dataframe 中
|
||||
if "&-sell_rsi_pred" not in dataframe.columns:
|
||||
logger.error("&-sell_rsi_pred 列仍然缺失,策略可能无法正常运行")
|
||||
raise ValueError("&-sell_rsi_pred 列缺失,无法继续执行策略逻辑")
|
||||
|
||||
# 生成 stoploss_pred(基于波动率)
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||||
dataframe["&-stoploss"] = (-0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)).fillna(-0.1)
|
||||
dataframe["&-stoploss"] = dataframe["&-stoploss"].clip(-0.35, -0.1)
|
||||
|
||||
# 生成 roi_0_pred(基于 ROI 参数)
|
||||
dataframe["&-roi_0"] = ((dataframe["close"] / dataframe["close"].shift(label_period) - 1).clip(0, 0.2)).fillna(0)
|
||||
|
||||
# 设置策略级参数
|
||||
try:
|
||||
self.buy_rsi.value = float(dataframe["&-buy_rsi_pred"].iloc[-1])
|
||||
self.sell_rsi.value = float(dataframe["&-sell_rsi_pred"].iloc[-1])
|
||||
except Exception as e:
|
||||
logger.error(f"设置 buy_rsi/sell_rsi 失败:{str(e)}")
|
||||
self.buy_rsi.value = 27
|
||||
self.sell_rsi.value = 59
|
||||
|
||||
self.stoploss = -0.15 # 固定止损 15%
|
||||
self.minimal_roi = {
|
||||
0: float(self.roi_0.value),
|
||||
15: float(self.roi_15.value),
|
||||
30: float(self.roi_30.value),
|
||||
60: 0
|
||||
}
|
||||
|
||||
# 更保守的追踪止损设置
|
||||
self.trailing_stop_positive = 0.05 # 追踪止损触发点
|
||||
self.trailing_stop_positive_offset = 0.1 # 追踪止损偏移量
|
||||
|
||||
logger.info(f"动态参数:buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, "
|
||||
f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}")
|
||||
|
||||
# 数据清理
|
||||
dataframe.replace([np.inf, -np.inf], 0, inplace=True)
|
||||
dataframe.ffill(inplace=True)
|
||||
dataframe.fillna(0, inplace=True)
|
||||
|
||||
return dataframe
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 改进卖出信号条件
|
||||
exit_long_conditions = [
|
||||
(df["rsi"] > df["&-sell_rsi_pred"]), # RSI 高于卖出阈值
|
||||
(df["volume"] > df["volume"].rolling(window=10).mean()), # 成交量高于近期均值
|
||||
(df["close"] < df["bb_middleband"]) # 价格低于布林带中轨
|
||||
]
|
||||
# 添加详细日志以验证 do_predict 的值
|
||||
if "do_predict" in df.columns:
|
||||
logger.debug(f"do_predict 列存在,前5行预览:\n{df[['do_predict']].head().to_string()}")
|
||||
else:
|
||||
logger.warning("do_predict 列不存在,可能未正确加载模型或进行预测。")
|
||||
|
||||
if exit_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, exit_long_conditions),
|
||||
"exit_long"
|
||||
] = 1
|
||||
if "&-buy_rsi_pred" in df.columns:
|
||||
logger.debug(f"&-buy_rsi_pred 列存在,前5行预览:\n{df[['&-buy_rsi_pred']].head().to_string()}")
|
||||
else:
|
||||
logger.warning("&-buy_rsi_pred 列不存在,可能未正确生成或被覆盖。")
|
||||
return df
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 改进买入信号条件
|
||||
# 检查 MACD 列是否存在
|
||||
if "macd" not in df.columns or "macdsignal" not in df.columns:
|
||||
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号。尝试重新计算 MACD 列。")
|
||||
|
||||
try:
|
||||
macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
df["macd"] = macd["macd"]
|
||||
df["macdsignal"] = macd["macdsignal"]
|
||||
logger.info("MACD 列已成功重新计算。")
|
||||
except Exception as e:
|
||||
logger.error(f"重新计算 MACD 列时出错:{str(e)}")
|
||||
raise ValueError("DataFrame 缺少必要的 MACD 列且无法重新计算。")
|
||||
|
||||
enter_long_conditions = [
|
||||
(df["rsi"] < df["&-buy_rsi_pred"]), # RSI 低于买入阈值
|
||||
(df["volume"] > df["volume"].rolling(window=10).mean() * 1.2), # 成交量高于近期均值20%
|
||||
(df["close"] > df["bb_middleband"]) # 价格高于布林带中轨
|
||||
]
|
||||
|
||||
# 如果 MACD 列存在,则添加 MACD 金叉条件
|
||||
if "macd" in df.columns and "macdsignal" in df.columns:
|
||||
enter_long_conditions.append((df["macd"] > df["macdsignal"]))
|
||||
|
||||
# 确保模型预测为买入
|
||||
enter_long_conditions.append((df["do_predict"] == 1))
|
||||
|
||||
# 添加详细日志以验证 do_predict 的值
|
||||
if "do_predict" in df.columns:
|
||||
logger.debug(f"do_predict 列存在,前5行预览:\n{df[['do_predict']].head().to_string()}")
|
||||
else:
|
||||
logger.warning("do_predict 列不存在,可能未正确加载模型或进行预测。")
|
||||
|
||||
if enter_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_long_conditions),
|
||||
["enter_long", "enter_tag"]
|
||||
] = (1, "long")
|
||||
return df
|
||||
|
||||
def confirm_trade_entry(
|
||||
self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time, entry_tag, side: str, **kwargs
|
||||
) -> bool:
|
||||
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = df.iloc[-1].squeeze()
|
||||
if side == "long":
|
||||
if rate > (last_candle["close"] * (1 + 0.0025)):
|
||||
return False
|
||||
return True
|
||||
@ -1,247 +0,0 @@
|
||||
import logging
|
||||
import numpy as np
|
||||
from functools import reduce
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical import qtpylib
|
||||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MLBasedSentimentStrategy(IStrategy):
|
||||
# 参数定义:MLBasedSentimentStrategy 动态适配 buy_rsi 和 sell_rsi,禁用 Hyperopt 优化
|
||||
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
|
||||
# 市场情绪参数
|
||||
sentiment_weight = DecimalParameter(low=0.1, high=0.9, default=0.5, space="buy", optimize=True, load=True)
|
||||
|
||||
# ROI 参数
|
||||
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.05, space="buy", optimize=True, load=True)
|
||||
roi_15 = DecimalParameter(low=0.01, high=0.15, default=0.03, space="buy", optimize=True, load=True)
|
||||
roi_30 = DecimalParameter(low=0.01, high=0.1, default=0.02, space="buy", optimize=True, load=True)
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
|
||||
# 保留关键的技术指标
|
||||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
|
||||
# 确保 MACD 列被正确计算并保留
|
||||
try:
|
||||
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe["macd"] = macd["macd"]
|
||||
dataframe["macdsignal"] = macd["macdsignal"]
|
||||
except Exception as e:
|
||||
logger.error(f"计算 MACD 列时出错:{str(e)}")
|
||||
dataframe["macd"] = np.nan
|
||||
dataframe["macdsignal"] = np.nan
|
||||
|
||||
# 检查 MACD 列是否存在
|
||||
if "macd" not in dataframe.columns or "macdsignal" not in dataframe.columns:
|
||||
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号")
|
||||
raise ValueError("DataFrame 缺少必要的 MACD 列")
|
||||
|
||||
# 确保 MACD 列存在
|
||||
if "macd" not in dataframe.columns or "macdsignal" not in dataframe.columns:
|
||||
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号")
|
||||
raise ValueError("DataFrame 缺少必要的 MACD 列")
|
||||
|
||||
# 保留布林带相关特征
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
|
||||
# 保留成交量相关特征
|
||||
dataframe["volume_ma"] = dataframe["volume"].rolling(window=20).mean()
|
||||
|
||||
# 添加市场情绪特征
|
||||
# 假设我们有一个外部数据源提供市场情绪分数
|
||||
# 这里我们使用一个示例值,实际应用中需要从外部数据源获取
|
||||
dataframe["sentiment_score"] = 0.5 # 示例值,实际应替换为真实数据
|
||||
|
||||
# 数据清理
|
||||
for col in dataframe.columns:
|
||||
if dataframe[col].dtype in ["float64", "int64"]:
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe[col] = dataframe[col].ffill().fillna(0)
|
||||
logger.info(f"特征工程完成,特征数量:{len(dataframe.columns)}")
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
|
||||
if "close" not in dataframe.columns:
|
||||
logger.error("数据框缺少必要的 'close' 列")
|
||||
raise ValueError("数据框缺少必要的 'close' 列")
|
||||
|
||||
try:
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
|
||||
# 定义目标变量为未来价格变化百分比(连续值)
|
||||
dataframe["up_or_down"] = (
|
||||
dataframe["close"].shift(-label_period) - dataframe["close"]
|
||||
) / dataframe["close"]
|
||||
|
||||
# 数据清理:处理 NaN 和 Inf 值
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
|
||||
|
||||
# 确保目标变量是二维数组
|
||||
if dataframe["up_or_down"].ndim == 1:
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].values.reshape(-1, 1)
|
||||
|
||||
# 检查并处理 NaN 或无限值
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe["up_or_down"] = dataframe["up_or_down"].ffill().fillna(0)
|
||||
|
||||
# 生成 %-volatility 特征
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||||
|
||||
# 确保 &-buy_rsi 列的值计算正确
|
||||
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
|
||||
# 数据清理
|
||||
for col in ["&-buy_rsi", "up_or_down", "%-volatility"]:
|
||||
# 使用直接操作避免链式赋值
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe[col] = dataframe[col].ffill() # 替代 fillna(method='ffill')
|
||||
dataframe[col] = dataframe[col].fillna(dataframe[col].mean()) # 使用均值填充 NaN 值
|
||||
if dataframe[col].isna().any():
|
||||
logger.warning(f"目标列 {col} 仍包含 NaN,填充为默认值")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||||
raise
|
||||
|
||||
# Log the shape of the target variable for debugging
|
||||
logger.info(f"目标列形状:{dataframe['up_or_down'].shape}")
|
||||
logger.info(f"目标列预览:\n{dataframe[['up_or_down', '&-buy_rsi']].head().to_string()}")
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.info(f"处理交易对:{metadata['pair']}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
# 计算传统指标
|
||||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
|
||||
# 生成 up_or_down 信号(非 FreqAI 目标)
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
# 使用未来价格变化方向生成 up_or_down 信号
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
dataframe["up_or_down"] = np.where(
|
||||
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
|
||||
)
|
||||
# 动态设置参数
|
||||
if "&-buy_rsi" in dataframe.columns:
|
||||
# 派生其他目标
|
||||
dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||||
# Ensure proper calculation and handle potential NaN values
|
||||
dataframe["&-stoploss"] = (-0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)).fillna(-0.1)
|
||||
dataframe["&-roi_0"] = ((dataframe["close"] / dataframe["close"].shift(label_period) - 1).clip(0, 0.2)).fillna(0)
|
||||
|
||||
# Additional check to ensure no NaN values remain
|
||||
for col in ["&-stoploss", "&-roi_0"]:
|
||||
if dataframe[col].isna().any():
|
||||
logger.warning(f"列 {col} 仍包含 NaN,填充为默认值")
|
||||
dataframe[col] = dataframe[col].fillna(-0.1 if col == "&-stoploss" else 0)
|
||||
# 简化动态参数生成逻辑
|
||||
# 放松 buy_rsi 和 sell_rsi 的生成逻辑
|
||||
# 计算 buy_rsi_pred 并清理 NaN 值
|
||||
dataframe["buy_rsi_pred"] = dataframe["rsi"].rolling(window=10).mean().clip(30, 50)
|
||||
dataframe["buy_rsi_pred"] = dataframe["buy_rsi_pred"].fillna(dataframe["buy_rsi_pred"].median())
|
||||
|
||||
# 计算 sell_rsi_pred 并清理 NaN 值
|
||||
dataframe["sell_rsi_pred"] = dataframe["buy_rsi_pred"] + 20
|
||||
dataframe["sell_rsi_pred"] = dataframe["sell_rsi_pred"].fillna(dataframe["sell_rsi_pred"].median())
|
||||
|
||||
# 计算 stoploss_pred 并清理 NaN 值
|
||||
dataframe["stoploss_pred"] = -0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)
|
||||
dataframe["stoploss_pred"] = dataframe["stoploss_pred"].fillna(dataframe["stoploss_pred"].mean())
|
||||
|
||||
# 计算 roi_0_pred 并清理 NaN 值
|
||||
dataframe["roi_0_pred"] = dataframe["&-roi_0"].clip(0.01, 0.2)
|
||||
dataframe["roi_0_pred"] = dataframe["roi_0_pred"].fillna(dataframe["roi_0_pred"].mean())
|
||||
# 检查预测值
|
||||
for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred", "&-sell_rsi", "&-stoploss", "&-roi_0"]:
|
||||
if dataframe[col].isna().any():
|
||||
logger.warning(f"列 {col} 包含 NaN,填充为默认值")
|
||||
dataframe[col] = dataframe[col].fillna(dataframe[col].mean())
|
||||
# 更保守的止损和止盈设置
|
||||
dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.3).clip(0.01, 0.2)
|
||||
dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3)
|
||||
# 设置策略级参数
|
||||
self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1])
|
||||
self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1])
|
||||
# 更保守的止损设置
|
||||
self.stoploss = -0.15 # 固定止损 15%
|
||||
self.minimal_roi = {
|
||||
0: float(self.roi_0.value),
|
||||
15: float(self.roi_15.value),
|
||||
30: float(self.roi_30.value),
|
||||
60: 0
|
||||
}
|
||||
# 更保守的追踪止损设置
|
||||
self.trailing_stop_positive = 0.05 # 追踪止损触发点
|
||||
self.trailing_stop_positive_offset = 0.1 # 追踪止损偏移量
|
||||
logger.info(f"动态参数:buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, "
|
||||
f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}")
|
||||
dataframe.replace([np.inf, -np.inf], 0, inplace=True)
|
||||
dataframe.ffill(inplace=True)
|
||||
dataframe.fillna(0, inplace=True)
|
||||
logger.info(f"up_or_down 值统计:\n{dataframe['up_or_down'].value_counts().to_string()}")
|
||||
logger.info(f"do_predict 值统计:\n{dataframe['do_predict'].value_counts().to_string()}")
|
||||
return dataframe
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 改进买入信号条件
|
||||
# 检查 MACD 列是否存在
|
||||
if "macd" not in df.columns or "macdsignal" not in df.columns:
|
||||
logger.error("MACD 或 MACD 信号列缺失,无法生成买入信号。尝试重新计算 MACD 列。")
|
||||
try:
|
||||
macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
df["macd"] = macd["macd"]
|
||||
df["macdsignal"] = macd["macdsignal"]
|
||||
logger.info("MACD 列已成功重新计算。")
|
||||
except Exception as e:
|
||||
logger.error(f"重新计算 MACD 列时出错:{str(e)}")
|
||||
raise ValueError("DataFrame 缺少必要的 MACD 列且无法重新计算。")
|
||||
|
||||
enter_long_conditions = [
|
||||
(df["rsi"] < df["buy_rsi_pred"]), # RSI 低于买入阈值
|
||||
(df["volume"] > df["volume"].rolling(window=10).mean() * 1.2), # 成交量高于近期均值20%
|
||||
(df["close"] > df["bb_middleband"]), # 价格高于布林带中轨
|
||||
(df["sentiment_score"] > self.sentiment_weight.value) # 市场情绪积极
|
||||
]
|
||||
|
||||
# 如果 MACD 列存在,则添加 MACD 金叉条件
|
||||
if "macd" in df.columns and "macdsignal" in df.columns:
|
||||
enter_long_conditions.append((df["macd"] > df["macdsignal"]))
|
||||
# 确保模型预测为买入
|
||||
enter_long_conditions.append((df["do_predict"] == 1))
|
||||
if enter_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, enter_long_conditions),
|
||||
["enter_long", "enter_tag"]
|
||||
] = (1, "long")
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
# 改进卖出信号条件
|
||||
exit_long_conditions = [
|
||||
(df["rsi"] > df["sell_rsi_pred"]), # RSI 高于卖出阈值
|
||||
(df["volume"] > df["volume"].rolling(window=10).mean()), # 成交量高于近期均值
|
||||
(df["close"] < df["bb_middleband"]), # 价格低于布林带中轨
|
||||
(df["sentiment_score"] < self.sentiment_weight.value) # 市场情绪消极
|
||||
]
|
||||
if exit_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, exit_long_conditions),
|
||||
"exit_long"
|
||||
] = 1
|
||||
return df
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
from freqtrade.strategy import IStrategy
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import logging
|
||||
from technical import qtpylib
|
||||
from functools import reduce
|
||||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MyDynamicStrategy(IStrategy):
|
||||
|
||||
# --- 参数空间 ---
|
||||
buy_rsi = IntParameter(10, 50, default=30)
|
||||
sell_rsi = IntParameter(50, 90, default=70)
|
||||
roi_0 = DecimalParameter(0.01, 0.2, default=0.03)
|
||||
roi_1 = DecimalParameter(0.005, 0.1, default=0.015)
|
||||
stoploss_param = DecimalParameter(-0.3, -0.1, default=-0.15)
|
||||
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.05
|
||||
trailing_stop_positive_offset = 0.1
|
||||
can_short = False
|
||||
|
||||
process_only_new_candles = True
|
||||
use_exit_signal = True
|
||||
stoploss = -0.15
|
||||
minimal_roi = {"0": 0.03, "30": 0.01, "60": 0}
|
||||
|
||||
# --- Plotting config ---
|
||||
plot_config = {
|
||||
"main_plot": {},
|
||||
"subplots": {
|
||||
"RSI Buy Threshold": {
|
||||
"&-buy_rsi": {"color": "green"}
|
||||
},
|
||||
"ROI and Stoploss": {
|
||||
"&-roi_0": {"color": "orange"},
|
||||
"&-stoploss": {"color": "red"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- FreqAI 配置 ---
|
||||
freqai_info = {
|
||||
"model": "CatboostClassifier",
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["5m", "1h"],
|
||||
"indicator_periods_candles": [10, 20, 50],
|
||||
"include_corr_pairlist": ["BTC/USDT"],
|
||||
"target_classifier": "value",
|
||||
"label_period_candles": 20,
|
||||
},
|
||||
"training_settings": {
|
||||
"train_period_days": 30,
|
||||
"startup_candle_count": 200
|
||||
}
|
||||
}
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
|
||||
df = dataframe.copy()
|
||||
df[f'rsi_{period}'] = ta.RSI(df, timeperiod=period)
|
||||
df[f'sma_diff_{period}'] = df['close'] - ta.SMA(df, timeperiod=period)
|
||||
df[f'macd_{period}'], _, _ = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
df[f'stoch_rsi_{period}'] = ta.STOCHRSI(df, timeperiod=period)
|
||||
df[f'cci_{period}'] = ta.CCI(df, timeperiod=period)
|
||||
df[f'willr_{period}'] = ta.WILLR(df, timeperiod=period)
|
||||
df[f'atr_{period}'] = ta.ATR(df, timeperiod=period)
|
||||
df[f'price_change_rate_{period}'] = df['close'].pct_change(period)
|
||||
df[f'volatility_{period}'] = df['close'].pct_change().rolling(window=period).std()
|
||||
return df
|
||||
|
||||
def set_freqai_targets(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
# 使用短期和长期均线交叉作为目标标签
|
||||
short_ma = ta.SMA(dataframe, timeperiod=10)
|
||||
long_ma = ta.SMA(dataframe, timeperiod=50)
|
||||
dataframe['target'] = np.where(short_ma > long_ma, 2,
|
||||
np.where(short_ma < long_ma, 0, 1))
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
# 示例:使用简单未来N周期涨跌作为目标变量
|
||||
# 使用短期均线趋势代替未来价格
|
||||
# 计算短期和长期均线
|
||||
short_ma = ta.SMA(dataframe, timeperiod=10)
|
||||
long_ma = ta.SMA(dataframe, timeperiod=50)
|
||||
dataframe['short_ma'] = short_ma
|
||||
dataframe['long_ma'] = long_ma
|
||||
|
||||
# 计算 RSI 和其他动态参数
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
dataframe['&-buy_rsi'] = self.buy_rsi.value
|
||||
dataframe['&-sell_rsi'] = self.sell_rsi.value
|
||||
dataframe['&-roi_0'] = self.roi_0.value
|
||||
dataframe['&-stoploss'] = self.stoploss_param.value
|
||||
|
||||
# 添加调试日志
|
||||
logging.info(f"Feature columns after feature engineering: {list(dataframe.columns)}")
|
||||
|
||||
# 使用短期和长期均线交叉作为目标标签
|
||||
dataframe['target'] = np.where(short_ma > long_ma, 2,
|
||||
np.where(short_ma < long_ma, 1, 0))
|
||||
|
||||
# 动态设置 minimal_roi
|
||||
# 平滑处理 ROI 参数
|
||||
# 基于波动率动态调整 ROI 参数
|
||||
# 使用指数加权移动平均 (EWMA) 计算波动率
|
||||
volatility = dataframe['close'].pct_change().ewm(span=20, adjust=False).std().mean()
|
||||
roi_0_dynamic = max(0.01, min(0.2, self.roi_0.value * (1 + volatility)))
|
||||
roi_1_dynamic = max(0.005, min(0.1, self.roi_1.value * (1 + volatility)))
|
||||
self.minimal_roi = {
|
||||
0: roi_0_dynamic,
|
||||
30: roi_1_dynamic,
|
||||
60: 0
|
||||
}
|
||||
# 动态调整止损距离
|
||||
volatility_multiplier = max(1.5, min(3.0, 2.0 + volatility))
|
||||
# 波动率倍数
|
||||
self.stoploss = -0.15 * volatility_multiplier
|
||||
|
||||
# 计算 Bollinger Bands 并添加到 dataframe
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bollinger_upper'] = bollinger['upper']
|
||||
dataframe['bollinger_mid'] = bollinger['mid']
|
||||
dataframe['bollinger_lower'] = bollinger['lower']
|
||||
|
||||
# 计算 MACD 并添加到 dataframe
|
||||
macd, macdsignal, _ = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe['macd'] = macd
|
||||
dataframe['macdsignal'] = macdsignal
|
||||
|
||||
# 添加调试日志
|
||||
logging.info(f"RSI condition: {(dataframe['rsi'] < dataframe['&-buy_rsi']).sum()}")
|
||||
logging.info(f"Volume condition: {(dataframe['volume'] > dataframe['volume'].rolling(window=20).mean() * 1.05).sum()}")
|
||||
logging.info(f"MACD condition: {((dataframe['close'] <= dataframe['bollinger_lower'] * 1.01) & (dataframe['macd'] > dataframe['macdsignal'])).sum()}")
|
||||
|
||||
|
||||
# 添加 ADX 趋势过滤器
|
||||
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
|
||||
is_strong_trend = dataframe['adx'].iloc[-1] > 25
|
||||
# MACD 穿越信号条件
|
||||
(dataframe["close"] < dataframe['bollinger_lower']) & (dataframe['macd'] > dataframe['macdsignal']),
|
||||
|
||||
# 基于趋势强度动态调整追踪止损
|
||||
trend_strength = (dataframe['short_ma'] - dataframe['long_ma']).mean()
|
||||
if is_strong_trend:
|
||||
self.trailing_stop_positive = max(0.01, min(0.1, abs(trend_strength) * 0.3))
|
||||
self.trailing_stop_positive_offset = max(0.01, min(0.2, abs(trend_strength) * 0.6))
|
||||
else:
|
||||
self.trailing_stop_positive = 0.05
|
||||
self.trailing_stop_positive_offset = 0.1
|
||||
trend_strength = (dataframe['short_ma'] - dataframe['long_ma']).mean()
|
||||
self.trailing_stop_positive = max(0.01, min(0.1, abs(trend_strength) * 0.3))
|
||||
|
||||
return dataframe
|
||||
def populate_entry_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
# 增加成交量过滤和 Bollinger Bands 信号
|
||||
# 计算 MACD
|
||||
macd, macdsignal, _ = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
df['macd'] = macd
|
||||
df['macdsignal'] = macdsignal
|
||||
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(df), window=20, stds=2)
|
||||
conditions = [
|
||||
(df["rsi"] < df["&-buy_rsi"]), # RSI 低于买入阈值
|
||||
(df["volume"] > df["volume"].rolling(window=20).mean() * 1.1), # 成交量增长超过 10%
|
||||
(df["close"] < df['bollinger_lower']) & (df['macd'] > df['macdsignal']), # MACD 穿越信号
|
||||
]
|
||||
df.loc[reduce(lambda x, y: x & y, conditions), 'enter_long'] = 1
|
||||
return df
|
||||
|
||||
def populate_exit_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
# 增加 Bollinger Bands 中轨信号
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(df), window=20, stds=2)
|
||||
exit_long_conditions = [
|
||||
(df["rsi"] > df["&-sell_rsi"]),
|
||||
(df["close"] > df['bollinger_mid']) # Bollinger Bands 中轨信号
|
||||
]
|
||||
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), 'exit_long'] = 1
|
||||
return df
|
||||
def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force, current_time, entry_tag, side, **kwargs):
|
||||
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = df.iloc[-1]
|
||||
if rate > last_candle["close"] * 1.0025:
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
from freqtrade.strategy import IStrategy, CategoricalParameter, DecimalParameter, IntParameter
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import talib.abstract as ta
|
||||
|
||||
|
||||
class MyHyperoptStrategy(IStrategy):
|
||||
INTERFACE_VERSION = 3
|
||||
|
||||
# Buy hyperspace params:
|
||||
buy_params = {
|
||||
"ema_short_period": 10,
|
||||
"ema_long_period": 50,
|
||||
}
|
||||
|
||||
# Sell hyperspace params:
|
||||
sell_params = {
|
||||
"rsi_high": 70,
|
||||
}
|
||||
|
||||
ema_short_period = IntParameter(5, 20, default=10, space="buy", optimize=True)
|
||||
ema_long_period = IntParameter(40, 100, default=50, space="buy", optimize=True)
|
||||
|
||||
rsi_high = IntParameter(60, 85, default=70, space="sell", optimize=True)
|
||||
|
||||
# Minimal ROI designed for the strategy
|
||||
minimal_roi = {
|
||||
"0": 0.1
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
stoploss = -0.10
|
||||
|
||||
# Trailing stop
|
||||
trailing_stop = False
|
||||
|
||||
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
# Ensure only the 'close' column is passed to RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
|
||||
dataframe['ema_short'] = dataframe['close'].ewm(span=self.ema_short_period.value, adjust=False).mean()
|
||||
dataframe['ema_long'] = dataframe['close'].ewm(span=self.ema_long_period.value, adjust=False).mean()
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['ema_short'] > dataframe['ema_long']) &
|
||||
(dataframe['rsi'] < 30)
|
||||
),
|
||||
'enter_long'
|
||||
] = 1
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['ema_short'] < dataframe['ema_long']) &
|
||||
(dataframe['rsi'] > self.rsi_high.value)
|
||||
),
|
||||
'exit_long'
|
||||
] = 1
|
||||
return dataframe
|
||||
@ -1,709 +0,0 @@
|
||||
import logging
|
||||
from freqtrade.strategy import IStrategy
|
||||
from pandas import DataFrame
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import talib as ta
|
||||
import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from sklearn.metrics import mean_squared_error
|
||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter
|
||||
from xgboost import XGBRegressor
|
||||
import ccxt
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OKXRegressionStrategy(IStrategy):
|
||||
"""
|
||||
Freqtrade AI 策略,使用回归模型进行 OKX 数据上的仅做多交易。
|
||||
- 数据通过 CCXT 从 OKX 交易所获取。
|
||||
- 使用 XGBoost 回归模型预测价格变化。
|
||||
- 仅生成做多(买入)信号,不做空。
|
||||
- 适配 Freqtrade 2025.3,继承 IStrategy。
|
||||
"""
|
||||
|
||||
# 指标所需的最大启动蜡烛数
|
||||
startup_candle_count: int = 20
|
||||
|
||||
# 策略元数据(建议通过 config.json 配置)
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.01
|
||||
max_open_trades = 3
|
||||
stake_amount = 'dynamic'
|
||||
atr_period = CategoricalParameter([7, 14, 21], default=14, space='buy')
|
||||
atr_multiplier = DecimalParameter(1.0, 3.0, default=2.0, space='sell')
|
||||
|
||||
# FreqAI 配置
|
||||
freqai_config = {
|
||||
"enabled": True,
|
||||
"identifier": "okx_regression_v1",
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 100,
|
||||
"learning_rate": 0.05,
|
||||
"max_depth": 6
|
||||
},
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["5m", "15m", "1h"],
|
||||
"include_corr_pairlist": ["BTC/USDT", "ETH/USDT"],
|
||||
"label_period_candles": 12,
|
||||
"include_shifted_candles": 2, # 添加历史偏移特征
|
||||
"principal_component_analysis": True # 启用 PCA
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.2,
|
||||
"random_state": 42,
|
||||
"shuffle": False
|
||||
},
|
||||
"train_period_days": 90,
|
||||
"backtest_period_days": 30,
|
||||
"purge_old_models": True # 清理旧模型
|
||||
}
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
super().__init__(config)
|
||||
# 初始化特征缓存
|
||||
self.feature_cache = {}
|
||||
# 设置日志级别
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 输出模型路径用于调试
|
||||
freqai_model_path = self.config.get("freqai", {}).get("model_path", "/freqtrade/user_data/models")
|
||||
logger.info(f"FreqAI 模型路径:{freqai_model_path}")
|
||||
|
||||
# 新增模型加载状态日志
|
||||
model_identifier = self.freqai_config.get("identifier", "default")
|
||||
model_file = f"{freqai_model_path}/{model_identifier}/cb_okb_*.pkl"
|
||||
import os
|
||||
if any(os.path.exists(f.replace('*', '1743465600')) for f in [model_file]):
|
||||
logger.info("✅ 模型文件存在,准备加载")
|
||||
else:
|
||||
logger.warning("⚠️ 模型文件未找到,请确认是否训练完成")
|
||||
logger.info(f"🔍 正在尝试从 {freqai_model_path} 加载模型")
|
||||
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: Dict, **kwargs) -> DataFrame:
|
||||
"""
|
||||
为每个时间框架和相关交易对生成特征。
|
||||
"""
|
||||
cache_key = f"{metadata.get('pair', 'unknown')}_{period}"
|
||||
if cache_key in self.feature_cache:
|
||||
logger.debug(f"使用缓存特征:{cache_key}")
|
||||
return self.feature_cache[cache_key].copy()
|
||||
|
||||
# RSI
|
||||
dataframe[f"rsi_{period}"] = ta.RSI(dataframe["close"], timeperiod=period)
|
||||
|
||||
# MACD
|
||||
macd, macdsignal, _ = ta.MACD(dataframe["close"], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe[f"%-%-macd-{period}"] = macd
|
||||
dataframe[f"%-%-macdsignal-{period}"] = macdsignal
|
||||
|
||||
# 布林带宽度
|
||||
upper, middle, lower = ta.BBANDS(dataframe["close"], timeperiod=period)
|
||||
dataframe[f"%-%-bb_width-{period}"] = (upper - lower) / middle
|
||||
|
||||
# 成交量均线
|
||||
dataframe[f"%-%-volume_ma-{period}"] = ta.SMA(dataframe["volume"], timeperiod=period)
|
||||
|
||||
# 仅为 BTC/USDT 和 ETH/USDT 生成 order_book_imbalance
|
||||
#
|
||||
pair = metadata.get('pair', 'unknown')
|
||||
# 注释掉订单簿相关代码
|
||||
# if pair in ["BTC/USDT", "ETH/USDT"]:
|
||||
# try:
|
||||
# order_book = self.fetch_okx_order_book(pair)
|
||||
# dataframe[f"%-%-order_book_imbalance"] = (
|
||||
# order_book["bids"] - order_book["asks"]
|
||||
# ) / (order_book["bids"] + order_book["asks"] + 1e-10)
|
||||
# except Exception as e:
|
||||
# logger.warning(f"Failed to fetch order book for {pair}: {str(e)}")
|
||||
# dataframe[f"%-%-order_book_imbalance"] = 0.0
|
||||
|
||||
# 数据清洗
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
|
||||
dataframe = dataframe.ffill().fillna(0)
|
||||
|
||||
# 缓存特征副本,避免引用污染
|
||||
self.feature_cache[cache_key] = dataframe.copy()
|
||||
logger.debug(f"周期 {period} 特征:{list(dataframe.filter(like='%-%-').columns)}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
"""
|
||||
添加基础时间框架的全局特征。
|
||||
"""
|
||||
# 确保索引是 DatetimeIndex
|
||||
if not isinstance(dataframe.index, pd.DatetimeIndex):
|
||||
dataframe = dataframe.set_index(pd.DatetimeIndex(dataframe.index))
|
||||
|
||||
# 价格变化率
|
||||
dataframe["%-price_change"] = dataframe["close"].pct_change()
|
||||
|
||||
# 时间特征:小时
|
||||
dataframe["%-hour_of_day"] = dataframe.index.hour / 24.0
|
||||
|
||||
# 数据清洗
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
|
||||
dataframe = dataframe.ffill().fillna(0)
|
||||
|
||||
logger.debug(f"全局特征:{list(dataframe.filter(like='%-%-').columns)}")
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
|
||||
"""
|
||||
设置回归模型的目标变量,为不同币对设置动态止损和ROI阈值。
|
||||
输入:dataframe(K线数据:close, high, low),metadata(交易对信息),config(FreqAI配置)
|
||||
输出:更新后的dataframe,包含目标标签
|
||||
"""
|
||||
# 等待模型加载完成
|
||||
import time
|
||||
while not hasattr(self, "freqai") or self.freqai is None:
|
||||
time.sleep(1)
|
||||
|
||||
# 获取配置参数
|
||||
label_period = self.freqai_config["feature_parameters"]["label_period_candles"] # 标签预测周期(如5分钟K线的N根)
|
||||
pair = metadata["pair"] # 当前交易对(如DOGE/USDT)
|
||||
|
||||
# 计算未来价格变化率(现有逻辑)
|
||||
dataframe["&-s_close"] = (dataframe["close"].shift(-label_period) - dataframe["close"]) / dataframe["close"]
|
||||
|
||||
# 计算不同时间窗口的ROI(现有逻辑)
|
||||
for minutes in [0, 15, 30]:
|
||||
candles = int(minutes / 5) # 假设5分钟K线
|
||||
if candles > 0:
|
||||
dataframe[f"&-roi_{minutes}"] = (dataframe["close"].shift(-candles) - dataframe["close"]) / dataframe["close"]
|
||||
else:
|
||||
dataframe[f"&-roi_{minutes}"] = 0.0
|
||||
|
||||
# 计算市场状态指标:ADX(14周期,与label_period_candles对齐)
|
||||
dataframe["adx"] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
|
||||
|
||||
# 定义币对特定的ADX阈值和止损/ROI范围
|
||||
pair_thresholds = {
|
||||
"DOGE/USDT": {
|
||||
"adx_trend": 20,
|
||||
"adx_oscillation": 15,
|
||||
"stoploss_trend": -0.08,
|
||||
"stoploss_oscillation": -0.04,
|
||||
"stoploss_mid": -0.06,
|
||||
"roi_trend": 0.06,
|
||||
"roi_oscillation": 0.025,
|
||||
"roi_mid": 0.04
|
||||
},
|
||||
"BTC/USDT": {
|
||||
"adx_trend": 25,
|
||||
"adx_oscillation": 20,
|
||||
"stoploss_trend": -0.03,
|
||||
"stoploss_oscillation": -0.015,
|
||||
"stoploss_mid": -0.02,
|
||||
"roi_trend": 0.03,
|
||||
"roi_oscillation": 0.015,
|
||||
"roi_mid": 0.02
|
||||
},
|
||||
"SOL/USDT": {
|
||||
"adx_trend": 22,
|
||||
"adx_oscillation": 18,
|
||||
"stoploss_trend": -0.06,
|
||||
"stoploss_oscillation": -0.03,
|
||||
"stoploss_mid": -0.045,
|
||||
"roi_trend": 0.045,
|
||||
"roi_oscillation": 0.02,
|
||||
"roi_mid": 0.03
|
||||
},
|
||||
"XRP/USDT": {
|
||||
"adx_trend": 22,
|
||||
"adx_oscillation": 18,
|
||||
"stoploss_trend": -0.06,
|
||||
"stoploss_oscillation": -0.03,
|
||||
"stoploss_mid": -0.045,
|
||||
"roi_trend": 0.045,
|
||||
"roi_oscillation": 0.02,
|
||||
"roi_mid": 0.03
|
||||
},
|
||||
"OKB/USDT": {
|
||||
"adx_trend": 18,
|
||||
"adx_oscillation": 12,
|
||||
"stoploss_trend": -0.10,
|
||||
"stoploss_oscillation": -0.06,
|
||||
"stoploss_mid": -0.08,
|
||||
"roi_trend": 0.07,
|
||||
"roi_oscillation": 0.04,
|
||||
"roi_mid": 0.05
|
||||
}
|
||||
}
|
||||
# 设置回归目标
|
||||
label_period = self.freqai_config["feature_parameters"]["label_period_candles"]
|
||||
dataframe["&-s_close"] = (dataframe["close"].shift(-label_period) - dataframe["close"]) / dataframe["close"]
|
||||
|
||||
# 设置动态 ROI 和止损阈值
|
||||
dataframe["&-roi_0_pred"] = 0.03 # 默认值,防止空值
|
||||
dataframe["&-stoploss_pred"] = -0.05
|
||||
|
||||
for index, row in dataframe.iterrows():
|
||||
thresholds = pair_thresholds.get(pair, {})
|
||||
if not thresholds:
|
||||
continue
|
||||
adx_value = row["adx"]
|
||||
if adx_value > thresholds["adx_trend"]: # 趋势市场
|
||||
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"] # 宽松止损
|
||||
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
|
||||
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"] # 严格止损
|
||||
else:
|
||||
# 中性市场:使用中间值
|
||||
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"]
|
||||
|
||||
for index, row in dataframe.iterrows():
|
||||
thresholds = pair_thresholds.get(pair, {})
|
||||
if not thresholds:
|
||||
continue
|
||||
adx_value = row["adx"]
|
||||
if adx_value > thresholds["adx_trend"]: # 趋势市场
|
||||
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"] # 较高ROI目标
|
||||
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
|
||||
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"] # 较低ROI目标
|
||||
else:
|
||||
# 中性市场:使用中间值
|
||||
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"]
|
||||
|
||||
return dataframe
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
|
||||
"""
|
||||
使用 FreqAI 生成指标和预测。
|
||||
"""
|
||||
|
||||
try:
|
||||
logger.debug(f"FreqAI 对象:{type(self.freqai)}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
# 强制刷新缓存,避免旧数据干扰
|
||||
if hasattr(self, 'feature_cache'):
|
||||
self.feature_cache.clear()
|
||||
|
||||
# 验证数据完整性
|
||||
if dataframe["close"].isna().any() or dataframe["volume"].isna().any():
|
||||
logger.warning("检测到 OKX 数据缺失,使用前向填充")
|
||||
dataframe = dataframe.ffill().fillna(0)
|
||||
|
||||
# 检查所有目标列是否都存在
|
||||
required_targets = ["&-s_close", "&-roi_0", "&-buy_rsi_pred", "&-stoploss_pred", "&-roi_0_pred"]
|
||||
missing_targets = [col for col in required_targets if col not in dataframe.columns]
|
||||
if missing_targets:
|
||||
logger.warning(f"⚠️ 缺失目标列:{missing_targets},尝试重新生成")
|
||||
|
||||
# 检查预测列
|
||||
required_preds = ['pred_upper', 'pred_lower']
|
||||
missing_preds = [col for col in required_preds if col not in dataframe.columns]
|
||||
if missing_preds:
|
||||
logger.warning(f"⚠️ 缺失预测列:{missing_preds},尝试回退默认值")
|
||||
for col in missing_preds:
|
||||
dataframe[col] = np.nan
|
||||
|
||||
# 预测统计
|
||||
if "&-s_close" in dataframe.columns:
|
||||
logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, 方差={dataframe['&-s_close'].var():.4f}")
|
||||
|
||||
if dataframe["pred_upper"].isna().all():
|
||||
logger.warning("⚠️ pred_upper 列全为 NaN,可能模型未加载成功")
|
||||
logger.debug(f"生成的列:{list(dataframe.columns)}")
|
||||
except Exception as e:
|
||||
logger.error(f"FreqAI start 失败:{str(e)}")
|
||||
raise
|
||||
finally:
|
||||
logger.debug("populate_indicators 完成")
|
||||
|
||||
# 确保返回 DataFrame,防止 None
|
||||
if dataframe is None:
|
||||
dataframe = DataFrame()
|
||||
|
||||
# 添加 ATR 指标
|
||||
dataframe['ATR_{}'.format(self.atr_period.value)] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
|
||||
|
||||
# 添加 RSI 和布林带指标
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
upper, middle, lower = ta.BBANDS(dataframe['close'], timeperiod=20, nbdevup=2, nbdevdn=2)
|
||||
dataframe['bb_upper'] = upper
|
||||
dataframe['bb_middle'] = middle
|
||||
dataframe['bb_lower'] = lower
|
||||
|
||||
# 添加高时间框架趋势(1小时均线)
|
||||
dataframe_1h = None
|
||||
if self.config['timeframe'] != '1h':
|
||||
dataframe_1h = self.dp.get_analyzed_dataframe(metadata['pair'], '1h')[0]
|
||||
if dataframe_1h is not None and not dataframe_1h.empty and 'close' in dataframe_1h.columns:
|
||||
dataframe['trend_1h'] = dataframe_1h['close'].rolling(window=20).mean()
|
||||
else:
|
||||
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
|
||||
else:
|
||||
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
|
||||
|
||||
# 强制检查预测列是否存在,若不存在则填充 NaN 并记录警告
|
||||
required_pred_cols = ['pred_upper', 'pred_lower', '&-s_close', '&-roi_0_pred']
|
||||
missing_pred_cols = [col for col in required_pred_cols if col not in dataframe.columns]
|
||||
|
||||
if missing_pred_cols:
|
||||
logger.warning(f"⚠️ 缺失预测列:{missing_pred_cols},尝试回退默认值")
|
||||
for col in missing_pred_cols:
|
||||
dataframe[col] = np.nan
|
||||
else:
|
||||
logger.debug("✅ 预测列已就绪:")
|
||||
logger.debug(dataframe[['&-s_close', 'pred_upper', 'pred_lower']].head().to_string())
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
|
||||
"""
|
||||
基于模型预测和 RSI 生成做多信号。
|
||||
"""
|
||||
# 确保必要列存在
|
||||
required_cols = ['rsi', 'volume', '&-s_close', '&-roi_0_pred']
|
||||
for col in required_cols:
|
||||
if col not in dataframe.columns:
|
||||
dataframe[col] = np.nan
|
||||
|
||||
# 只在预测值有效时生成信号
|
||||
valid_prediction = (~dataframe['&-s_close'].isna()) & (dataframe['&-s_close'] > 0.005) & (dataframe['&-s_close'].abs() > 0.001)
|
||||
|
||||
# 结合 RSI 和模型预测生成信号
|
||||
dataframe.loc[
|
||||
(dataframe["rsi"] < 30) &
|
||||
(dataframe["volume"] > 0) &
|
||||
valid_prediction,
|
||||
"enter_long"
|
||||
] = 1
|
||||
|
||||
# 设置 entry_price 列,用于止损逻辑
|
||||
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
|
||||
|
||||
logger.debug(f"生成 {dataframe['enter_long'].sum()} 个做多信号")
|
||||
return dataframe
|
||||
def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
|
||||
"""
|
||||
封装动态止损逻辑,基于入场价和ATR计算止损线
|
||||
:param dataframe: 原始DataFrame
|
||||
:param metadata: 策略元数据
|
||||
:param atr_col: 使用的ATR列名
|
||||
:param multiplier: ATR乘数
|
||||
:return: 更新后的DataFrame
|
||||
"""
|
||||
# 获取交易对信息
|
||||
pair = metadata.get('pair', 'unknown')
|
||||
|
||||
# 设置默认的止损倍数
|
||||
stop_loss_multiplier = 2.0
|
||||
|
||||
# 根据交易对调整止损倍数(示例:BTC/USDT 更稳定,止损倍数较低)
|
||||
if pair == "BTC/USDT":
|
||||
stop_loss_multiplier = 1.5
|
||||
elif pair == "DOGE/USDT":
|
||||
stop_loss_multiplier = 2.5
|
||||
elif pair == "SOL/USDT":
|
||||
stop_loss_multiplier = 2.0
|
||||
elif pair == "XRP/USDT":
|
||||
stop_loss_multiplier = 2.0
|
||||
|
||||
# 计算止损线
|
||||
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
|
||||
dataframe['stop_loss_line'] = dataframe['entry_price'] - dataframe[atr_col] * stop_loss_multiplier
|
||||
|
||||
# 应用止损逻辑(OKB/USDT 使用更平滑的退出)
|
||||
if metadata.get('pair') == 'OKB/USDT':
|
||||
# 对 OKB 添加缓冲区,避免频繁触发
|
||||
buffer_ratio = 0.005 # 0.5% 缓冲
|
||||
buffered_stop_loss = dataframe['stop_loss_line'] * (1 - buffer_ratio)
|
||||
dataframe.loc[
|
||||
(dataframe['close'] < buffered_stop_loss),
|
||||
'exit_long'
|
||||
] = 1
|
||||
else:
|
||||
dataframe.loc[
|
||||
(dataframe['close'] < dataframe['stop_loss_line']),
|
||||
'exit_long'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def _dynamic_take_profit(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
|
||||
"""
|
||||
封装动态止盈逻辑,基于入场价、ATR 和 ADX 调整止盈线
|
||||
:param dataframe: 原始DataFrame
|
||||
:param metadata: 策略元数据
|
||||
:param atr_col: 使用的ATR列名
|
||||
:param multiplier: ATR乘数(基础值)
|
||||
:return: 更新后的DataFrame
|
||||
"""
|
||||
# 获取交易对信息
|
||||
pair = metadata.get('pair', 'unknown')
|
||||
|
||||
# 设置默认的止盈倍数
|
||||
take_profit_multiplier = 2.0
|
||||
|
||||
# 根据交易对调整止盈倍数(示例:BTC/USDT 更稳定,止盈倍数较高)
|
||||
if pair == "BTC/USDT":
|
||||
take_profit_multiplier = 3.0
|
||||
elif pair == "DOGE/USDT":
|
||||
take_profit_multiplier = 1.5
|
||||
elif pair == "SOL/USDT":
|
||||
take_profit_multiplier = 2.0
|
||||
elif pair == "XRP/USDT":
|
||||
take_profit_multiplier = 2.0
|
||||
|
||||
# 计算当前ATR在历史窗口中的百分位
|
||||
historical_atr = dataframe[atr_col].rolling(window=20).mean().dropna().values
|
||||
if len(historical_atr) < 20:
|
||||
return dataframe
|
||||
|
||||
current_atr = dataframe[atr_col].iloc[-1]
|
||||
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
|
||||
|
||||
# 根据波动率百分位调整止盈倍数
|
||||
if percentile > 80: # 高波动市场,缩短止盈距离
|
||||
volatility_adjustment = 0.8
|
||||
elif percentile < 20: # 低波动市场,拉长止盈距离
|
||||
volatility_adjustment = 1.2
|
||||
else: # 正常波动市场,保持默认
|
||||
volatility_adjustment = 1.0
|
||||
|
||||
# 获取ADX指标判断趋势强度
|
||||
dataframe['adx'] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
|
||||
adx_value = dataframe['adx'].iloc[-1]
|
||||
|
||||
# 根据ADX趋势强度调整止盈倍数
|
||||
if adx_value > 25: # 强趋势,延长止盈距离
|
||||
trend_adjustment = 1.3
|
||||
elif adx_value < 15: # 震荡行情,缩短止盈距离
|
||||
trend_adjustment = 0.7
|
||||
else: # 中性趋势,保持默认
|
||||
trend_adjustment = 1.0
|
||||
|
||||
# 综合调整止盈倍数
|
||||
adjusted_multiplier = take_profit_multiplier * volatility_adjustment * trend_adjustment
|
||||
|
||||
# 计算止盈线
|
||||
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
|
||||
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe[atr_col] * adjusted_multiplier
|
||||
|
||||
# 应用止盈逻辑
|
||||
dataframe.loc[
|
||||
(dataframe['close'] > dataframe['take_profit_line']),
|
||||
'exit_long'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
|
||||
# 确保 ATR 列存在
|
||||
if 'ATR_14' not in dataframe.columns:
|
||||
dataframe['ATR_14'] = 0.0
|
||||
|
||||
# 应用动态止损和止盈逻辑
|
||||
dataframe = self._dynamic_stop_loss(dataframe, metadata)
|
||||
dataframe = self._dynamic_take_profit(dataframe, metadata)
|
||||
|
||||
return dataframe
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
"""
|
||||
动态下注:每笔交易占账户余额 2%。
|
||||
"""
|
||||
balance = self.wallets.get_available_stake_amount()
|
||||
stake = balance * 0.02
|
||||
return min(max(stake, min_stake), max_stake)
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, profit_percent: float,
|
||||
after_fill: bool, **kwargs) -> Optional[float]:
|
||||
"""
|
||||
自适应止损:基于市场波动率百分位动态调整ATR乘数
|
||||
"""
|
||||
if trade.enter_tag == 'long':
|
||||
# 获取多个周期的ATR值
|
||||
dataframe = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)
|
||||
|
||||
# 计算不同周期的ATR
|
||||
dataframe['ATR_7'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=7)
|
||||
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
|
||||
dataframe['ATR_21'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=21)
|
||||
|
||||
# 计算20日平均ATR(用于百分位计算)
|
||||
avg_atr_window = 20
|
||||
dataframe['avg_atr'] = dataframe['ATR_14'].rolling(window=avg_atr_window).mean()
|
||||
|
||||
# 获取最新数据
|
||||
latest_row = dataframe.iloc[-1].copy()
|
||||
|
||||
# 计算当前ATR在历史窗口中的百分位
|
||||
historical_atr = dataframe['avg_atr'].dropna().values
|
||||
if len(historical_atr) < avg_atr_window:
|
||||
return None
|
||||
|
||||
current_atr = latest_row['avg_atr']
|
||||
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
|
||||
|
||||
# 根据市场波动率百分位选择ATR乘数
|
||||
if percentile > 80: # 高波动市场
|
||||
atr_multiplier = 1.5
|
||||
elif percentile < 20: # 低波动市场
|
||||
atr_multiplier = 2.5
|
||||
else: # 正常波动市场
|
||||
atr_multiplier = 2.0
|
||||
|
||||
# 根据交易对调整基础ATR值
|
||||
pair_specific_atr = {
|
||||
"BTC/USDT": latest_row['ATR_14'],
|
||||
"ETH/USDT": latest_row['ATR_14'],
|
||||
"OKB/USDT": latest_row['ATR_14'], # 使用更长周期 ATR 减少波动影响
|
||||
"TON/USDT": latest_row['ATR_7']
|
||||
}
|
||||
|
||||
if pair in pair_specific_atr:
|
||||
base_atr = pair_specific_atr[pair]
|
||||
else:
|
||||
base_atr = latest_row['ATR_14']
|
||||
|
||||
# 计算追踪止损价格
|
||||
trailing_stop = current_rate - base_atr * atr_multiplier
|
||||
|
||||
# 添加额外条件:确保止损不低于入场价的一定比例
|
||||
min_profit_ratio = 0.005 # 最低盈利0.5%
|
||||
min_stop_price = trade.open_rate * (1 + min_profit_ratio)
|
||||
final_stop_price = max(trailing_stop, min_stop_price)
|
||||
|
||||
return final_stop_price / current_rate - 1 # 返回相对百分比
|
||||
|
||||
return None
|
||||
|
||||
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, side: str,
|
||||
**kwargs) -> float:
|
||||
"""
|
||||
禁用杠杆,仅做多。
|
||||
"""
|
||||
return 1.0
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
|
||||
"""
|
||||
验证交易进入,检查 OKX 数据新鲜度。
|
||||
"""
|
||||
if not self.check_data_freshness(pair, current_time):
|
||||
logger.warning(f"{pair} 的 OKX 数据过期,跳过交易")
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_data_freshness(self, pair: str, current_time: 'datetime') -> bool:
|
||||
"""
|
||||
简化版数据新鲜度检查,不依赖外部 API。
|
||||
"""
|
||||
# 假设数据总是新鲜的(用于测试)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def fit(self, data_dictionary: Dict, metadata: Dict, **kwargs) -> None:
|
||||
"""
|
||||
训练回归模型并记录性能。
|
||||
"""
|
||||
try:
|
||||
# 初始化模型
|
||||
if not hasattr(self, 'model') or self.model is None:
|
||||
model_params = self.freqai_config["model_training_parameters"]
|
||||
self.model = XGBRegressor(**model_params)
|
||||
logger.debug("初始化新的 XGBoost 回归模型")
|
||||
|
||||
# 调用 FreqAI 训练
|
||||
self.freqai.fit(data_dictionary, metadata, **kwargs)
|
||||
|
||||
# 记录训练集性能
|
||||
train_data = data_dictionary["train_features"]
|
||||
train_labels = data_dictionary["train_labels"]
|
||||
train_predictions = self.model.predict(train_data)
|
||||
train_mse = mean_squared_error(train_labels, train_predictions)
|
||||
logger.info(f"训练集 MSE:{train_mse:.6f}")
|
||||
|
||||
# 记录测试集性能(如果可用)
|
||||
if "test_features" in data_dictionary:
|
||||
test_data = data_dictionary["test_features"]
|
||||
test_labels = data_dictionary["test_labels"]
|
||||
test_predictions = self.model.predict(test_data)
|
||||
test_mse = mean_squared_error(test_labels, test_predictions)
|
||||
logger.info(f"测试集 MSE:{test_mse:.6f}")
|
||||
|
||||
# 特征重要性
|
||||
if hasattr(self.model, 'feature_importances_'):
|
||||
importance = self.model.feature_importances_
|
||||
logger.debug(f"特征重要性:{dict(zip(train_data.columns, importance))}")
|
||||
except Exception as e:
|
||||
logger.error(f"FreqAI fit 失败:{str(e)}")
|
||||
raise
|
||||
raise RuntimeError("模型训练失败,请检查数据完整性或重新训练模型")
|
||||
def _callback_stop_loss(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
动态回调止损逻辑:基于ATR调整回撤阈值,并结合RSI和布林带过滤信号。
|
||||
"""
|
||||
pair = metadata.get('pair', 'unknown')
|
||||
|
||||
# 设置默认参数
|
||||
atr_col = 'ATR_14'
|
||||
rolling_high_period = 20
|
||||
rsi_overbought = 70
|
||||
|
||||
# 设置不同币种的回调乘数
|
||||
callback_multipliers = {
|
||||
"BTC/USDT": 1.5,
|
||||
"ETH/USDT": 2.0,
|
||||
"OKB/USDT": 1.3,
|
||||
"TON/USDT": 2.0,
|
||||
}
|
||||
callback_multiplier = callback_multipliers.get(pair, 2.0)
|
||||
|
||||
# 确保ATR列存在
|
||||
if atr_col not in dataframe.columns:
|
||||
dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
|
||||
|
||||
# 计算动态回调百分比(基于ATR)
|
||||
dataframe['callback_threshold'] = dataframe[atr_col] * callback_multiplier
|
||||
|
||||
# 计算ATR
|
||||
if 'ATR_14' not in dataframe.columns:
|
||||
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
|
||||
|
||||
# 计算最近高点
|
||||
dataframe['rolling_high'] = dataframe['close'].rolling(window=20).max()
|
||||
|
||||
# 计算回调阈值
|
||||
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe['ATR_14'] * callback_multiplier
|
||||
|
||||
# 应用止盈逻辑
|
||||
dataframe.loc[
|
||||
(dataframe['close'] > dataframe['take_profit_line']),
|
||||
'exit_long'
|
||||
] = 1
|
||||
|
||||
# 计算当前价格相对于最近高点的回撤比例(使用ATR标准化)
|
||||
dataframe['callback_ratio'] = (dataframe['close'] - dataframe['rolling_high']) / dataframe['rolling_high']
|
||||
dataframe['callback_condition_atr'] = (dataframe['close'] - dataframe['rolling_high']) <= -dataframe['callback_threshold']
|
||||
|
||||
# 获取RSI和布林带信息
|
||||
dataframe['in_overbought'] = dataframe['rsi'] > rsi_overbought
|
||||
dataframe['below_bb_upper'] = dataframe['close'] < dataframe['bb_upper']
|
||||
|
||||
# 获取高时间框架趋势(1小时均线)
|
||||
dataframe['trend_up'] = dataframe['close'] > dataframe['trend_1h']
|
||||
dataframe['trend_down'] = dataframe['close'] < dataframe['trend_1h']
|
||||
|
||||
# 综合回调止损条件
|
||||
callback_condition = (
|
||||
dataframe['callback_condition_atr'] &
|
||||
((dataframe['in_overbought'] | (~dataframe['below_bb_upper']))) &
|
||||
dataframe['trend_down']
|
||||
)
|
||||
|
||||
# 应用回调止损逻辑
|
||||
dataframe.loc[callback_condition, 'exit_long'] = 1
|
||||
|
||||
return dataframe
|
||||
@ -1,192 +0,0 @@
|
||||
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
|
||||
from pandas import DataFrame
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
import numpy as np
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TheForceFreqaiStrategy(IStrategy):
|
||||
timeframe = '5m'
|
||||
minimal_roi = {"0": 0.05, "60": 0.03, "120": 0.01}
|
||||
stoploss = -0.15
|
||||
process_only_new_candles = True
|
||||
use_exit_signal = True
|
||||
startup_candle_count = 200
|
||||
max_open_trades = 4
|
||||
|
||||
buy_macd_diff = DecimalParameter(-10.0, 10.0, default=0.0, space="buy")
|
||||
sell_stoch_overbought = IntParameter(70, 90, default=80, space="sell")
|
||||
|
||||
freqai = {
|
||||
"enabled": True,
|
||||
"model": "LightGBMRegressor",
|
||||
"train_period_days": 7,
|
||||
"backtest_period_days": 2,
|
||||
"identifier": "theforce_model",
|
||||
"feature_params": {
|
||||
"include_timeframes": ["5m"],
|
||||
"include_corr_pairlist": [],
|
||||
"label_period_candles": 5,
|
||||
"include_shifted_candles": 2,
|
||||
"DI_threshold": 1.5,
|
||||
"include_default_features": ["open", "high", "low", "close", "volume"],
|
||||
"use_SVM_to_remove_outliers": False
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.2,
|
||||
"random_state": 42
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 100,
|
||||
"max_depth": 7,
|
||||
"verbose": -1
|
||||
},
|
||||
"live_retrain": True,
|
||||
"purge_old_models": True,
|
||||
"fit_live_predictions_candles": 50,
|
||||
"data_kitchen_thread_count": 2,
|
||||
"force_train": True,
|
||||
"verbose": 2,
|
||||
"save_backtest_models": True
|
||||
}
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
super().__init__(config)
|
||||
self.feature_cache = {}
|
||||
self.freqai_config = self.freqai
|
||||
logging.getLogger('').setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
for log in ['freqtrade.freqai', 'freqtrade.freqai.data_kitchen',
|
||||
'freqtrade.freqai.data_drawer', 'freqtrade.freqai.prediction_models']:
|
||||
logging.getLogger(log).setLevel(logging.DEBUG)
|
||||
logger.info("Initialized TheForceFreqaiStrategy with DEBUG logging")
|
||||
|
||||
def _normalize_column(self, series: pd.Series) -> pd.Series:
|
||||
if series.nunique() <= 1:
|
||||
return pd.Series(np.zeros_like(series), index=series.index)
|
||||
min_val = series.min()
|
||||
max_val = series.max()
|
||||
normalized = (series - min_val) / (max_val - min_val)
|
||||
return normalized.fillna(0)
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.debug(f"Populating indicators for pair {metadata['pair']}")
|
||||
logger.debug(f"Input dataframe shape: {dataframe.shape}, columns: {list(dataframe.columns)}")
|
||||
logger.debug(f"First 5 rows:\n{dataframe.head().to_string()}")
|
||||
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
logger.info("FreqAI start called, checking dataframe columns: %s", list(dataframe.columns))
|
||||
|
||||
required_columns = ['open', 'high', 'low', 'close', 'volume']
|
||||
missing_columns = [col for col in required_columns if col not in dataframe.columns]
|
||||
if missing_columns:
|
||||
logger.error(f"Missing columns in dataframe: {missing_columns}")
|
||||
return dataframe
|
||||
if dataframe.empty or len(dataframe) < self.startup_candle_count:
|
||||
logger.error(f"Insufficient data: {len(dataframe)} candles, required {self.startup_candle_count}")
|
||||
return dataframe
|
||||
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd'].fillna(0)
|
||||
dataframe['macdsignal'] = macd['macdsignal'].fillna(0)
|
||||
dataframe['macd_diff'] = macd['macd'] - macd['macdsignal']
|
||||
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowk'] = stoch['slowk'].fillna(0)
|
||||
dataframe['slowd'] = stoch['slowd'].fillna(0)
|
||||
|
||||
dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=12).fillna(dataframe['close'])
|
||||
dataframe['ema_long'] = ta.EMA(dataframe, timeperiod=26).fillna(dataframe['close'])
|
||||
|
||||
logger.debug(f"Indicators generated: {list(dataframe.columns)}")
|
||||
logger.debug(f"Indicator sample:\n{dataframe[['macd', 'slowk', 'ema_short']].tail().to_string()}")
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.debug(f"Setting FreqAI targets for pair {metadata['pair']}")
|
||||
label_candles = self.freqai_config['feature_params']['label_period_candles']
|
||||
dataframe['&-target'] = (
|
||||
dataframe['close'].shift(-label_candles) / dataframe['close'] - 1
|
||||
).fillna(0)
|
||||
valid_targets = len(dataframe['&-target'].dropna())
|
||||
logger.debug(f"Target '&-target' generated with {valid_targets} valid values")
|
||||
logger.debug(f"Target stats: min={dataframe['&-target'].min()}, max={dataframe['&-target'].max()}, mean={dataframe['&-target'].mean()}")
|
||||
logger.debug(f"Target sample:\n{dataframe['&-target'].tail().to_string()}")
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_expand(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.debug(f"Expanding features for pair {metadata['pair']}")
|
||||
dataframe['%-rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['%-bb_lowerband'] = bollinger['lower']
|
||||
dataframe['%-bb_middleband'] = bollinger['mid']
|
||||
dataframe['%-bb_upperband'] = bollinger['upper']
|
||||
dataframe['%-pct_change'] = dataframe['close'].pct_change()
|
||||
dataframe['%-volume_change'] = dataframe['volume'].pct_change(periods=5)
|
||||
|
||||
for col in dataframe.columns:
|
||||
if col.startswith('%-'):
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe[col] = dataframe[col].ffill().fillna(0)
|
||||
if dataframe[col].nunique() > 1:
|
||||
dataframe[f"{col}_norm"] = self._normalize_column(dataframe[col])
|
||||
else:
|
||||
dataframe[f"{col}_norm"] = 0
|
||||
|
||||
logger.debug(f"Features generated: {list(dataframe.columns)}")
|
||||
logger.debug(f"Feature sample:\n{dataframe[[c for c in dataframe.columns if c.startswith('%-')]].tail().to_string()}")
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.debug(f"Standard feature engineering for pair {metadata['pair']}")
|
||||
dataframe['%-rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
dataframe['%-day_of_week'] = dataframe['date'].dt.dayofweek
|
||||
dataframe['%-hour_of_day'] = dataframe['date'].dt.hour
|
||||
|
||||
for col in dataframe.columns:
|
||||
if col.startswith('%-'):
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], np.nan)
|
||||
dataframe[col] = dataframe[col].ffill().fillna(0)
|
||||
|
||||
logger.debug(f"Standard features generated: {list(dataframe.columns)}")
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.debug(f"Populating entry trend for pair {metadata['pair']}")
|
||||
logger.debug(f"Columns available: {list(dataframe.columns)}")
|
||||
logger.debug(f"&-target stats: {dataframe['&-target'].describe()}")
|
||||
logger.debug(f"slowk stats: {dataframe['slowk'].describe()}")
|
||||
logger.debug(f"macd_diff stats: {dataframe['macd_diff'].describe()}")
|
||||
|
||||
if '&-target' not in dataframe.columns:
|
||||
logger.warning(f"'&-target' column missing for pair {metadata['pair']}, using indicators only")
|
||||
dataframe.loc[
|
||||
(dataframe['macd'] > dataframe['macdsignal']) &
|
||||
(dataframe['macd_diff'] > self.buy_macd_diff.value) &
|
||||
(dataframe['slowk'] < 30) &
|
||||
(dataframe['ema_short'] > dataframe['ema_long']),
|
||||
'enter_long'] = 1
|
||||
logger.debug(f"Fallback entry signals: {dataframe['enter_long'].sum()} buys")
|
||||
return dataframe
|
||||
|
||||
dataframe.loc[
|
||||
(dataframe['macd'] > dataframe['macdsignal']) &
|
||||
(dataframe['macd_diff'] > self.buy_macd_diff.value) &
|
||||
(dataframe['slowk'] < 30) &
|
||||
(dataframe['ema_short'] > dataframe['ema_long']) &
|
||||
(dataframe['&-target'] > 0.005),
|
||||
'enter_long'] = 1
|
||||
logger.debug(f"FreqAI entry signals: {dataframe['enter_long'].sum()} buys")
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.debug(f"Populating exit trend for pair {metadata['pair']}")
|
||||
dataframe.loc[
|
||||
(dataframe['macd'] < dataframe['macdsignal']) &
|
||||
(dataframe['slowk'] > self.sell_stoch_overbought.value),
|
||||
'exit_long'] = 1
|
||||
return dataframe
|
||||
29
freqtrade/templates/TheForceV7.json
Normal file
29
freqtrade/templates/TheForceV7.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"strategy_name": "TheForceV7",
|
||||
"params": {
|
||||
"roi": {},
|
||||
"trailing": {
|
||||
"trailing_stop": false,
|
||||
"trailing_stop_positive": null,
|
||||
"trailing_stop_positive_offset": 0.0,
|
||||
"trailing_only_offset_is_reached": false
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 3
|
||||
},
|
||||
"buy": {
|
||||
"adx_buy": 25,
|
||||
"atr_ratio": 0.005
|
||||
},
|
||||
"sell": {
|
||||
"ema_fast_period": 7,
|
||||
"rsi_sell": 60
|
||||
},
|
||||
"protection": {},
|
||||
"stoploss": {
|
||||
"stoploss": -0.061
|
||||
}
|
||||
},
|
||||
"ft_stratparam_v": 1,
|
||||
"export_time": "2025-05-15 12:57:22.019801+00:00"
|
||||
}
|
||||
@ -1,18 +1,44 @@
|
||||
from freqtrade.strategy import IStrategy
|
||||
from pandas import DataFrame
|
||||
import talib.abstract as ta
|
||||
from typing import Optional, Union
|
||||
from typing import Dict, List, Optional, Union
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
||||
from datetime import datetime
|
||||
|
||||
class TheForceV7(IStrategy):
|
||||
# 基础参数
|
||||
timeframe = '5m'
|
||||
stoploss = -0.14 # 全局止损
|
||||
use_exit_signal = True
|
||||
exit_profit_only = False
|
||||
ignore_roi_if_entry_signal = False
|
||||
|
||||
# # Hyperopt 参数
|
||||
# stoploss = DecimalParameter(
|
||||
# low=-0.3, high=-0.1, default=-0.233, decimals=3, space='`stoploss'
|
||||
# )
|
||||
# profit_threshold_multiplier = DecimalParameter(
|
||||
# low=0.1, high=1.0, default=0.5, decimals=3, space='stoploss'
|
||||
# )
|
||||
# trailing_stop_multiplier = DecimalParameter(
|
||||
# low=0.5, high=3.0, default=1.5, decimals=3, space='stoploss'
|
||||
# )
|
||||
|
||||
# Hyperopt 参数
|
||||
stoploss = DecimalParameter(low=-0.3, high=-0.1, default=-0.233, decimals=3, space='stoploss')
|
||||
profit_threshold_multiplier = DecimalParameter(low=0.2, high=1.0, default=0.5, decimals=3, space='stoploss') # 扩大范围
|
||||
trailing_stop_multiplier = DecimalParameter(low=0.8, high=3.0, default=1.5, decimals=3, space='stoploss') # 扩大范围
|
||||
|
||||
# Hyperopt 参数
|
||||
stoploss = DecimalParameter(-0.3, -0.05, default=-0.15, decimals=3, space='stoploss')
|
||||
profit_threshold_multiplier = DecimalParameter(0.8, 3.0, default=1.5, decimals=3, space='stoploss')
|
||||
trailing_stop_multiplier = DecimalParameter(1.5, 6.0, default=3.0, decimals=3, space='stoploss')
|
||||
adx_buy = DecimalParameter(15, 35, default=25, decimals=0, space='buy')
|
||||
atr_ratio = DecimalParameter(0.002, 0.01, default=0.005, decimals=3, space='buy')
|
||||
rsi_sell = DecimalParameter(50, 70, default=60, decimals=0, space='sell')
|
||||
ema_fast_period = IntParameter(3, 10, default=7, space='sell')
|
||||
|
||||
|
||||
@property
|
||||
def protections(self):
|
||||
return [
|
||||
@ -39,18 +65,23 @@ class TheForceV7(IStrategy):
|
||||
"lookback_period_candles": 6,
|
||||
"trade_limit": 2,
|
||||
"stop_duration_candles": 60,
|
||||
"required_profit": 0.02 # 最低平均收益 2%
|
||||
"required_profit": 0.02
|
||||
},
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 2,
|
||||
"required_profit": 0.01 # 最低平均收益 1%
|
||||
"required_profit": 0.01
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# Calculate ATR (14-period, consistent with your code)
|
||||
|
||||
# Store the latest ATR in metadata for the pair
|
||||
|
||||
dataframe['ema200c'] = ta.EMA(dataframe['close'], timeperiod=200)
|
||||
dataframe['ema50c'] = ta.EMA(dataframe['close'], timeperiod=50)
|
||||
dataframe['ema20c'] = ta.EMA(dataframe['close'], timeperiod=20)
|
||||
@ -61,9 +92,16 @@ class TheForceV7(IStrategy):
|
||||
stoch = ta.STOCH(dataframe['high'], dataframe['low'], dataframe['close'], fastk_period=14, slowk_period=3, slowd_period=3)
|
||||
dataframe['slowk'] = stoch[0]
|
||||
dataframe['slowd'] = stoch[1]
|
||||
dataframe['adx'] = ta.ADX(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
|
||||
dataframe['volvar'] = dataframe['volume'].rolling(window=20).mean()
|
||||
|
||||
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
||||
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
for period in [self.ema_fast_period.value, 21, 50]:
|
||||
dataframe[f'ema_{period}'] = ta.EMA(dataframe, timeperiod=period)
|
||||
metadata['latest_atr'] = dataframe['atr'].iloc[-1]
|
||||
metadata['latest_adx'] = dataframe['adx'].iloc[-1]
|
||||
|
||||
return dataframe
|
||||
|
||||
def crossover(self, series1: DataFrame, series2: DataFrame) -> DataFrame:
|
||||
@ -77,7 +115,7 @@ class TheForceV7(IStrategy):
|
||||
(dataframe['rsi7'] < 50) & # Relaxed RSI
|
||||
(dataframe['macd'] > 0) & # Relaxed MACD
|
||||
(dataframe['volume'] > dataframe['volvar'] * 0.5) & # Relaxed volume
|
||||
(dataframe['adx'] > 17), # Trend strength
|
||||
(dataframe['adx'] > 21), # Trend strength
|
||||
'enter_long'
|
||||
] = 1
|
||||
|
||||
@ -97,43 +135,38 @@ class TheForceV7(IStrategy):
|
||||
|
||||
return dataframe
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
if dataframe.empty or dataframe['date'].iloc[-1] < current_time: # Fixed: Use date column
|
||||
return None
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> Optional[float]:
|
||||
atr = kwargs.get('metadata', {}).get('latest_atr', 0.01 * current_rate)
|
||||
adx = kwargs.get('metadata', {}).get('latest_adx', 0)
|
||||
atr_percent = atr / current_rate
|
||||
profit_threshold = float(self.profit_threshold_multiplier.value) * atr_percent
|
||||
trailing_stop_distance = float(self.trailing_stop_multiplier.value) * atr_percent
|
||||
trade_duration = (current_time - trade.open_date).total_seconds() / 3600
|
||||
|
||||
last_candle = dataframe.iloc[-1]
|
||||
atr = ta.ATR(dataframe, timeperiod=14).iloc[-1]
|
||||
duration = (current_time - trade.open_date).total_seconds() / 60 # Minutes
|
||||
# 动态调整
|
||||
if trade_duration < 3: # 3小时内放宽
|
||||
profit_threshold *= 1.5 # 提高触发门槛
|
||||
trailing_stop_distance *= 1.5 # 放宽止损距离
|
||||
if adx > 35: # 强趋势
|
||||
trailing_stop_distance *= 0.5 # 紧跟趋势
|
||||
elif adx < 20: # 震荡市场
|
||||
trailing_stop_distance *= 2.0 # 放宽止损
|
||||
|
||||
# Dynamic Take-Profit
|
||||
take_profit = current_rate + 1.0 * atr # Lowered ATR
|
||||
if current_rate >= take_profit:
|
||||
return "take_profit"
|
||||
# ATR 追踪止损
|
||||
if adx > 35 and after_fill and current_profit > profit_threshold:
|
||||
return -trailing_stop_distance
|
||||
# 固定止损
|
||||
elif trade_duration > 1.5 or adx < 20 or current_profit < -0.015: # 1.5小时,震荡,亏损>1.5%
|
||||
return float(self.stoploss)
|
||||
return -0.05 # 默认止损收紧
|
||||
|
||||
# Partial Take-Profit at 2%
|
||||
if current_profit >= 0.02:
|
||||
return "partial_take_profit"
|
||||
|
||||
# Dynamic Stop-Loss
|
||||
stop_loss = current_rate - 2.0 * atr # Relaxed ATR
|
||||
if current_rate <= stop_loss:
|
||||
return "dynamic_stop_loss"
|
||||
|
||||
# Trailing Stop
|
||||
if current_profit > 0.005: # Lowered threshold
|
||||
self.trailing_stop = True
|
||||
self.trailing_stop_positive = 0.003 # 0.3% retracement
|
||||
self.trailing_stop_positive_offset = 0.008 # 0.8% offset
|
||||
if current_profit < trade.max_profit - self.trailing_stop_positive:
|
||||
return "trailing_stop"
|
||||
|
||||
# Time Stop
|
||||
if duration > 60: # 1 hour
|
||||
return "time_stop"
|
||||
|
||||
return None
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int, min_date: datetime, max_date: datetime, config: Dict, processed: Dict[str, DataFrame], *args, **kwargs) -> float:
|
||||
total_profit = results['profit_abs'].sum()
|
||||
win_rate = len(results[results['profit_abs'] > 0]) / trade_count if trade_count > 0 else 0
|
||||
avg_duration = results['trade_duration'].mean() / 60 # 分钟
|
||||
loss = -total_profit * win_rate * (1 / (avg_duration + 1))
|
||||
return loss
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
@ -143,7 +176,7 @@ class TheForceV7(IStrategy):
|
||||
if dataframe.empty:
|
||||
return proposed_stake
|
||||
|
||||
atr = ta.ATR(dataframe, timeperiod=14).iloc[-1]
|
||||
atr = ta.ATR(dataframe, timeperiod=28).iloc[-1]
|
||||
price_std = dataframe['close'].std()
|
||||
combined_volatility = atr + price_std
|
||||
|
||||
@ -157,3 +190,4 @@ class TheForceV7(IStrategy):
|
||||
risk_factor = 1.2 if pair in ['BTC/USDT', 'ETH/USDT'] else 1.0
|
||||
|
||||
return base_stake * risk_factor
|
||||
|
||||
@ -1,183 +0,0 @@
|
||||
```python
|
||||
|
||||
class FreqaiExampleStrategy(IStrategy):
|
||||
minimal_roi = {}
|
||||
stoploss = -0.1
|
||||
trailing_stop = True
|
||||
process_only_new_candles = True
|
||||
use_exit_signal = True
|
||||
startup_candle_count: int = 100 # 增加数据需求
|
||||
can_short = False
|
||||
|
||||
# Hyperopt 参数
|
||||
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
|
||||
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, load=True)
|
||||
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.027, space="roi", optimize=True, load=True)
|
||||
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.009, space="roi", optimize=True, load=True)
|
||||
stoploss_param = DecimalParameter(low=-0.25, high=-0.05, default=-0.1, space="stoploss", optimize=True, load=True)
|
||||
|
||||
# 保护机制
|
||||
protections = [
|
||||
{"method": "StoplossGuard", "stop_duration": 60, "lookback_period": 120},
|
||||
{"method": "MaxDrawdown", "lookback_period": 120, "max_allowed_drawdown": 0.05}
|
||||
]
|
||||
|
||||
# FreqAI 配置
|
||||
freqai_info = {
|
||||
"model": "LightGBMRegressor",
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["5m", "15m", "1h"],
|
||||
"include_corr_pairlist": [],
|
||||
"label_period_candles": 12,
|
||||
"include_shifted_candles": 3,
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.2,
|
||||
"shuffle": False,
|
||||
"n_splits": 5 # 添加交叉验证
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 200,
|
||||
"learning_rate": 0.05,
|
||||
"num_leaves": 10,
|
||||
"min_child_weight": 1,
|
||||
"verbose": -1,
|
||||
},
|
||||
}
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
|
||||
if "close" not in dataframe.columns:
|
||||
logger.error("数据框缺少必要的 'close' 列")
|
||||
raise ValueError("数据框缺少必要的 'close' 列")
|
||||
|
||||
try:
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||||
dataframe["&-buy_rsi"] = (dataframe["close"].shift(-label_period) / dataframe["close"] - 1) * 100 # 修改目标为收益率
|
||||
|
||||
for col in ["&-buy_rsi", "%-volatility"]:
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
|
||||
dataframe[col] = dataframe[col].ffill()
|
||||
if dataframe[col].isna().any():
|
||||
logger.warning(f"目标列 {col} 包含 NaN,填充为 0")
|
||||
dataframe[col] = dataframe[col].fillna(0)
|
||||
logger.info(f"目标列 {col} 统计:\n{dataframe[col].describe().to_string()}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||||
raise
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.info(f"处理交易对:{metadata['pair']}, 数据形状:{dataframe.shape}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
# 计算传统指标
|
||||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# 检查FreqAI预测列
|
||||
if "&-buy_rsi_pred" in dataframe.columns:
|
||||
logger.info(f"&-buy_rsi_pred 统计:均值={dataframe['&-buy_rsi_pred'].mean():.2f}, 标准差={dataframe['&-buy_rsi_pred'].std():.2f}")
|
||||
dataframe["buy_rsi_trend"] = np.where(
|
||||
dataframe["&-buy_rsi_pred"] > dataframe["&-buy_rsi_pred"].shift(1), 1, 0
|
||||
)
|
||||
dataframe["&-sell_rsi_pred"] = dataframe["&-buy_rsi_pred"] + 30
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||||
dataframe["&-stoploss_pred"] = -0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)
|
||||
dataframe["&-roi_0_pred"] = (dataframe["&-buy_rsi_pred"] / 1000).clip(0.01, 0.2)
|
||||
|
||||
for col in ["&-buy_rsi_pred", "&-sell_rsi_pred", "&-stoploss_pred", "&-roi_0_pred"]:
|
||||
if dataframe[col].isna().any():
|
||||
logger.warning(f"列 {col} 包含 NaN,填充为默认值")
|
||||
mean_value = dataframe[col].mean()
|
||||
if pd.isna(mean_value):
|
||||
mean_value = {
|
||||
"&-buy_rsi_pred": 30,
|
||||
"&-sell_rsi_pred": 70,
|
||||
"&-stoploss_pred": -0.1,
|
||||
"&-roi_0_pred": 0.05
|
||||
}.get(col, 0)
|
||||
dataframe[col] = dataframe[col].fillna(mean_value)
|
||||
else:
|
||||
logger.warning(f"&-buy_rsi_pred 列缺失,使用默认值初始化")
|
||||
dataframe["buy_rsi_trend"] = 0
|
||||
dataframe["&-buy_rsi_pred"] = 30
|
||||
dataframe["&-sell_rsi_pred"] = 70
|
||||
dataframe["&-stoploss_pred"] = -0.1
|
||||
dataframe["&-roi_0_pred"] = 0.05
|
||||
|
||||
# 动态参数设置
|
||||
try:
|
||||
last_valid_idx = dataframe["&-stoploss_pred"].last_valid_index()
|
||||
if last_valid_idx is None:
|
||||
raise ValueError("没有有效的预测数据")
|
||||
self.stoploss = float(np.clip(dataframe["&-stoploss_pred"].iloc[last_valid_idx], -0.25, -0.05))
|
||||
self.buy_rsi.value = int(np.clip(dataframe["&-buy_rsi_pred"].iloc[last_valid_idx], 10, 50))
|
||||
self.sell_rsi.value = int(np.clip(dataframe["&-sell_rsi_pred"].iloc[last_valid_idx], 50, 90))
|
||||
self.roi_0.value = float(np.clip(dataframe["&-roi_0_pred"].iloc[last_valid_idx], 0.01, 0.2))
|
||||
self.minimal_roi = {
|
||||
0: self.roi_0.value,
|
||||
15: self.roi_15.value,
|
||||
30: self.roi_30.value,
|
||||
60: 0.0
|
||||
}
|
||||
logger.info(f"动态参数设置: buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, stoploss={self.stoploss:.2%}")
|
||||
except Exception as e:
|
||||
logger.error(f"动态参数设置失败,使用默认值: {str(e)}")
|
||||
self.stoploss = -0.1
|
||||
self.buy_rsi.value = 27
|
||||
self.sell_rsi.value = 59
|
||||
self.minimal_roi = {0: 0.038, 15: 0.027, 30: 0.009, 60: 0.0}
|
||||
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||||
dataframe = dataframe.ffill()
|
||||
dataframe = dataframe.fillna(0)
|
||||
|
||||
logger.info(f"do_predict 分布:\n{dataframe['do_predict'].value_counts().to_string()}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
enter_long_conditions = [
|
||||
qtpylib.crossed_above(df["rsi"], df["&-buy_rsi_pred"] - 5), # 放宽RSI条件
|
||||
df["tema"] > df["tema"].shift(1),
|
||||
df["volume"] > 0,
|
||||
df["do_predict"] == 1
|
||||
]
|
||||
if enter_long_conditions:
|
||||
condition_met = reduce(lambda x, y: x & y, enter_long_conditions)
|
||||
df.loc[condition_met, ["enter_long", "enter_tag"]] = (1, "long")
|
||||
if condition_met.any():
|
||||
logger.info(f"买入信号触发:{metadata['pair']},时间={df.index[condition_met][-1]}")
|
||||
else:
|
||||
logger.debug(f"买入条件未满足:{metadata['pair']},do_predict={df['do_predict'].iloc[-1]}, rsi={df['rsi'].iloc[-1]:.2f}, buy_rsi_pred={df['&-buy_rsi_pred'].iloc[-1]:.2f}")
|
||||
return df
|
||||
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
exit_long_conditions = [
|
||||
qtpylib.crossed_above(df["rsi"], df["&-sell_rsi_pred"]),
|
||||
df["close"] < df["bb_lowerband"],
|
||||
df["volume"] > 0,
|
||||
df["do_predict"] == 1
|
||||
]
|
||||
time_exit = (df["date"] >= df["date"].shift(1) + pd.Timedelta(days=2))
|
||||
df.loc[
|
||||
(reduce(lambda x, y: x & y, exit_long_conditions)) | time_exit,
|
||||
"exit_long"
|
||||
] = 1
|
||||
return df
|
||||
```
|
||||
```
|
||||
```
|
||||
```
|
||||
```
|
||||
```
|
||||
```
|
||||
```
|
||||
@ -1,69 +0,0 @@
|
||||
{%set volume_pairlist = '{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 1800
|
||||
}' %}
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": {{ max_open_trades }},
|
||||
"stake_currency": "{{ stake_currency }}",
|
||||
"stake_amount": {{ stake_amount }},
|
||||
"tradable_balance_ratio": 0.99,
|
||||
{{- ('\n "fiat_display_currency": "' + fiat_display_currency + '",') if fiat_display_currency else ''}}
|
||||
{{- ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
|
||||
"dry_run": {{ dry_run | lower }},
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"trading_mode": "{{ trading_mode }}",
|
||||
"margin_mode": "{{ margin_mode }}",
|
||||
"unfilledtimeout": {
|
||||
"entry": 10,
|
||||
"exit": 10,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"price_last_balance": 0.0,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "same",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
{{ exchange | indent(4) }},
|
||||
"pairlists": [
|
||||
{{ volume_pairlist }}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": {{ telegram | lower }},
|
||||
"token": "{{ telegram_token }}",
|
||||
"chat_id": "{{ telegram_chat_id }}"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": {{ api_server | lower }},
|
||||
"listen_ip_address": "{{ api_server_listen_addr | default("127.0.0.1", true) }}",
|
||||
"listen_port": 8080,
|
||||
"verbosity": "error",
|
||||
"enable_openapi": false,
|
||||
"jwt_secret_key": "{{ api_server_jwt_key }}",
|
||||
"ws_token": "{{ api_server_ws_token }}",
|
||||
"CORS_origins": [],
|
||||
"username": "{{ api_server_username }}",
|
||||
"password": "{{ api_server_password }}"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
}
|
||||
@ -1,179 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
# flake8: noqa: F401
|
||||
# isort: skip_file
|
||||
# --- Do not remove these imports ---
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pandas import DataFrame
|
||||
from typing import Dict, Optional, Union, Tuple
|
||||
|
||||
from freqtrade.strategy import (
|
||||
IStrategy,
|
||||
Trade,
|
||||
Order,
|
||||
PairLocks,
|
||||
informative, # @informative decorator
|
||||
# Hyperopt Parameters
|
||||
BooleanParameter,
|
||||
CategoricalParameter,
|
||||
DecimalParameter,
|
||||
IntParameter,
|
||||
RealParameter,
|
||||
# timeframe helpers
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_next_date,
|
||||
timeframe_to_prev_date,
|
||||
# Strategy helper functions
|
||||
merge_informative_pair,
|
||||
stoploss_from_absolute,
|
||||
stoploss_from_open,
|
||||
)
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta
|
||||
import pandas_ta as pta
|
||||
from technical import qtpylib
|
||||
|
||||
|
||||
class {{ strategy }}(IStrategy):
|
||||
"""
|
||||
This is a strategy template to get you started.
|
||||
More information in https://www.freqtrade.io/en/latest/strategy-customization/
|
||||
|
||||
You can:
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
- Rename the class name (Do not forget to update class_name)
|
||||
- Add any methods you want to build your strategy
|
||||
- Add any lib you need to build your strategy
|
||||
|
||||
You must keep:
|
||||
- the lib in the section "Do not remove these libs"
|
||||
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
|
||||
You should keep:
|
||||
- timeframe, minimal_roi, stoploss, trailing_*
|
||||
"""
|
||||
# Strategy interface version - allow new iterations of the strategy interface.
|
||||
# Check the documentation or the Sample strategy to get the latest version.
|
||||
INTERFACE_VERSION = 3
|
||||
|
||||
# Optimal timeframe for the strategy.
|
||||
timeframe = "5m"
|
||||
|
||||
# Can this strategy go short?
|
||||
can_short: bool = False
|
||||
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi".
|
||||
minimal_roi = {
|
||||
"60": 0.01,
|
||||
"30": 0.02,
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "stoploss".
|
||||
stoploss = -0.10
|
||||
|
||||
# Trailing stoploss
|
||||
trailing_stop = False
|
||||
# trailing_only_offset_is_reached = False
|
||||
# trailing_stop_positive = 0.01
|
||||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||
|
||||
# Run "populate_indicators()" only for new candle.
|
||||
process_only_new_candles = True
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_exit_signal = True
|
||||
exit_profit_only = False
|
||||
ignore_roi_if_entry_signal = False
|
||||
|
||||
# Number of candles the strategy requires before producing valid signals
|
||||
startup_candle_count: int = 30
|
||||
|
||||
# Strategy parameters
|
||||
buy_rsi = IntParameter(10, 40, default=30, space="buy")
|
||||
sell_rsi = IntParameter(60, 90, default=70, space="sell")
|
||||
|
||||
{{- attributes | indent(4) }}
|
||||
{{- plot_config | indent(4) }}
|
||||
|
||||
def informative_pairs(self):
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
These pair/interval combinations are non-tradeable, unless they are part
|
||||
of the whitelist as well.
|
||||
For more information, please consult the documentation
|
||||
:return: List of tuples in the format (pair, interval)
|
||||
Sample: return [("ETH/USDT", "5m"),
|
||||
("BTC/USDT", "15m"),
|
||||
]
|
||||
"""
|
||||
return []
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
{{- indicators | indent(8) }}
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the entry signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with entry columns populated
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
{{ buy_trend | indent(16) }}
|
||||
(dataframe["volume"] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
"enter_long"] = 1
|
||||
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
{{ sell_trend | indent(16) }}
|
||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
'enter_short'] = 1
|
||||
"""
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the exit signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with exit columns populated
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
{{ sell_trend | indent(16) }}
|
||||
(dataframe["volume"] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
"exit_long"] = 1
|
||||
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
{{ buy_trend | indent(16) }}
|
||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
'exit_short'] = 1
|
||||
"""
|
||||
return dataframe
|
||||
{{- additional_methods | indent(4) }}
|
||||
@ -1,419 +0,0 @@
|
||||
13c13,14
|
||||
< import time
|
||||
---
|
||||
>
|
||||
>
|
||||
31c32
|
||||
< trailing_stop_positive = 0.02 # 2% for aggressive profit-taking
|
||||
---
|
||||
> trailing_stop_positive = 0.01
|
||||
35c36
|
||||
< atr_multiplier = DecimalParameter(1.0, 3.0, default=2.5, space='sell') # 2.5 for lenient stop-loss
|
||||
---
|
||||
> atr_multiplier = DecimalParameter(1.0, 3.0, default=2.0, space='sell')
|
||||
50,51c51,52
|
||||
< "include_shifted_candles": 2,
|
||||
< "principal_component_analysis": True
|
||||
---
|
||||
> "include_shifted_candles": 2, # 添加历史偏移特征
|
||||
> "principal_component_analysis": True # 启用 PCA
|
||||
60c61
|
||||
< "purge_old_models": True
|
||||
---
|
||||
> "purge_old_models": True # 清理旧模型
|
||||
64a66
|
||||
> # 初始化特征缓存
|
||||
66c68
|
||||
< self.order_book_cache = {} # Cache for order book data
|
||||
---
|
||||
>
|
||||
72,81d73
|
||||
< # Ensure DatetimeIndex
|
||||
< if not isinstance(dataframe.index, pd.DatetimeIndex):
|
||||
< logger.warning("Index is not DatetimeIndex in feature_engineering_expand_all")
|
||||
< if 'date' in dataframe.columns:
|
||||
< dataframe.set_index(pd.to_datetime(dataframe['date']), inplace=True)
|
||||
< dataframe.drop(columns=['date'], inplace=True)
|
||||
< else:
|
||||
< logger.error("No 'date' column and index is not DatetimeIndex")
|
||||
< raise ValueError("Invalid DataFrame index")
|
||||
<
|
||||
102c94,95
|
||||
< # Order book imbalance for BTC/USDT and ETH/USDT
|
||||
---
|
||||
> # 仅为 BTC/USDT 和 ETH/USDT 生成 order_book_imbalance
|
||||
> #
|
||||
112c105
|
||||
< dataframe[f"%-%-order_book_imbalance"] = 0.0
|
||||
---
|
||||
> dataframe[f"%-%-order_book_imbalance"] = 0.0
|
||||
128c121
|
||||
< # Ensure DatetimeIndex
|
||||
---
|
||||
> # 确保索引是 DatetimeIndex
|
||||
130,136c123
|
||||
< logger.warning("Index is not DatetimeIndex in feature_engineering_standard")
|
||||
< if 'date' in dataframe.columns:
|
||||
< dataframe.set_index(pd.to_datetime(dataframe['date']), inplace=True)
|
||||
< dataframe.drop(columns=['date'], inplace=True)
|
||||
< else:
|
||||
< logger.error("No 'date' column and index is not DatetimeIndex")
|
||||
< raise ValueError("Invalid DataFrame index")
|
||||
---
|
||||
> dataframe = dataframe.set_index(pd.DatetimeIndex(dataframe.index))
|
||||
153a141,142
|
||||
> 输入:dataframe(K线数据:close, high, low),metadata(交易对信息),config(FreqAI配置)
|
||||
> 输出:更新后的dataframe,包含目标标签
|
||||
155,161c144,146
|
||||
< # Ensure DatetimeIndex
|
||||
< if not isinstance(dataframe.index, pd.DatetimeIndex):
|
||||
< logger.error("Invalid index in set_freqai_targets")
|
||||
< raise ValueError("DataFrame index must be DatetimeIndex")
|
||||
<
|
||||
< label_period = self.freqai_config["feature_parameters"]["label_period_candles"]
|
||||
< pair = metadata["pair"]
|
||||
---
|
||||
> # 获取配置参数
|
||||
> label_period = self.freqai_config["feature_parameters"]["label_period_candles"] # 标签预测周期(如5分钟K线的N根)
|
||||
> pair = metadata["pair"] # 当前交易对(如DOGE/USDT)
|
||||
163c148
|
||||
< # 计算未来价格变化率
|
||||
---
|
||||
> # 计算未来价格变化率(现有逻辑)
|
||||
166c151
|
||||
< # 计算不同时间窗口的ROI
|
||||
---
|
||||
> # 计算不同时间窗口的ROI(现有逻辑)
|
||||
168c153
|
||||
< candles = int(minutes / 5)
|
||||
---
|
||||
> candles = int(minutes / 5) # 假设5分钟K线
|
||||
174c159
|
||||
< # 计算ADX
|
||||
---
|
||||
> # 计算市场状态指标:ADX(14周期,与label_period_candles对齐)
|
||||
177c162
|
||||
< # 币对特定的ADX阈值和止损/ROI范围
|
||||
---
|
||||
> # 定义币对特定的ADX阈值和止损/ROI范围
|
||||
180,187c165,172
|
||||
< "adx_trend": 25,
|
||||
< "adx_oscillation": 15,
|
||||
< "stoploss_trend": -0.10,
|
||||
< "stoploss_oscillation": -0.06,
|
||||
< "stoploss_mid": -0.08,
|
||||
< "roi_trend": 0.10,
|
||||
< "roi_oscillation": 0.04,
|
||||
< "roi_mid": 0.07
|
||||
---
|
||||
> "adx_trend": 20, # 趋势市场ADX阈值
|
||||
> "adx_oscillation": 15, # 震荡市场ADX阈值
|
||||
> "stoploss_trend": -0.08, # 趋势市场止损:-8%
|
||||
> "stoploss_oscillation": -0.04, # 震荡市场止损:-4%
|
||||
> "stoploss_mid": -0.06, # 中间状态止损:-6%
|
||||
> "roi_trend": 0.06, # 趋势市场ROI:6%
|
||||
> "roi_oscillation": 0.025, # 震荡市场ROI:2.5%
|
||||
> "roi_mid": 0.04 # 中间状态ROI:4%
|
||||
191,197c176,182
|
||||
< "adx_oscillation": 15,
|
||||
< "stoploss_trend": -0.05,
|
||||
< "stoploss_oscillation": -0.025,
|
||||
< "stoploss_mid": -0.035,
|
||||
< "roi_trend": 0.05,
|
||||
< "roi_oscillation": 0.025,
|
||||
< "roi_mid": 0.035
|
||||
---
|
||||
> "adx_oscillation": 20,
|
||||
> "stoploss_trend": -0.03,
|
||||
> "stoploss_oscillation": -0.015,
|
||||
> "stoploss_mid": -0.02,
|
||||
> "roi_trend": 0.03,
|
||||
> "roi_oscillation": 0.015,
|
||||
> "roi_mid": 0.02
|
||||
200,207c185,192
|
||||
< "adx_trend": 25,
|
||||
< "adx_oscillation": 15,
|
||||
< "stoploss_trend": -0.08,
|
||||
< "stoploss_oscillation": -0.04,
|
||||
< "stoploss_mid": -0.06,
|
||||
< "roi_trend": 0.08,
|
||||
< "roi_oscillation": 0.035,
|
||||
< "roi_mid": 0.055
|
||||
---
|
||||
> "adx_trend": 22,
|
||||
> "adx_oscillation": 18,
|
||||
> "stoploss_trend": -0.06,
|
||||
> "stoploss_oscillation": -0.03,
|
||||
> "stoploss_mid": -0.045,
|
||||
> "roi_trend": 0.045,
|
||||
> "roi_oscillation": 0.02,
|
||||
> "roi_mid": 0.03
|
||||
210,237c195,202
|
||||
< "adx_trend": 25,
|
||||
< "adx_oscillation": 15,
|
||||
< "stoploss_trend": -0.07,
|
||||
< "stoploss_oscillation": -0.035,
|
||||
< "stoploss_mid": -0.05,
|
||||
< "roi_trend": 0.07,
|
||||
< "roi_oscillation": 0.03,
|
||||
< "roi_mid": 0.05
|
||||
< },
|
||||
< "OKB/USDT": {
|
||||
< "adx_trend": 25,
|
||||
< "adx_oscillation": 15,
|
||||
< "stoploss_trend": -0.10,
|
||||
< "stoploss_oscillation": -0.06,
|
||||
< "stoploss_mid": -0.08,
|
||||
< "roi_trend": 0.10,
|
||||
< "roi_oscillation": 0.04,
|
||||
< "roi_mid": 0.07
|
||||
< },
|
||||
< "TON/USDT": {
|
||||
< "adx_trend": 25,
|
||||
< "adx_oscillation": 15,
|
||||
< "stoploss_trend": -0.07,
|
||||
< "stoploss_oscillation": -0.04,
|
||||
< "stoploss_mid": -0.055,
|
||||
< "roi_trend": 0.07,
|
||||
< "roi_oscillation": 0.03,
|
||||
< "roi_mid": 0.05
|
||||
---
|
||||
> "adx_trend": 22,
|
||||
> "adx_oscillation": 18,
|
||||
> "stoploss_trend": -0.06,
|
||||
> "stoploss_oscillation": -0.03,
|
||||
> "stoploss_mid": -0.045,
|
||||
> "roi_trend": 0.045,
|
||||
> "roi_oscillation": 0.02,
|
||||
> "roi_mid": 0.03
|
||||
241c206
|
||||
< # 动态止损
|
||||
---
|
||||
> # 动态化 &-stoploss_pred(基于市场状态和币对)
|
||||
248,255c213,221
|
||||
< if adx_value > thresholds["adx_trend"]:
|
||||
< dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"]
|
||||
< elif adx_value < thresholds["adx_oscillation"]:
|
||||
< dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"]
|
||||
< else:
|
||||
< dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"]
|
||||
< if dataframe.at[index, "&-stoploss_pred"] < -0.12:
|
||||
< dataframe.at[index, "&-stoploss_pred"] = -0.12
|
||||
---
|
||||
> if adx_value > thresholds["adx_trend"]: # 趋势市场
|
||||
> dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"] # 宽松止损
|
||||
> elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
|
||||
> dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"] # 严格止损
|
||||
> else: # 中间状态
|
||||
> dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"] # 中等止损
|
||||
> # 风险控制:设置止损下限
|
||||
> if dataframe.at[index, "&-stoploss_pred"] < -0.10:
|
||||
> dataframe.at[index, "&-stoploss_pred"] = -0.10
|
||||
257c223
|
||||
< # 动态ROI
|
||||
---
|
||||
> # 动态化 &-roi_0_pred(基于市场趋势和币对)
|
||||
264,271c230,238
|
||||
< if adx_value > thresholds["adx_trend"]:
|
||||
< dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"]
|
||||
< elif adx_value < thresholds["adx_oscillation"]:
|
||||
< dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"]
|
||||
< else:
|
||||
< dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"]
|
||||
< if dataframe.at[index, "&-roi_0_pred"] > 0.15:
|
||||
< dataframe.at[index, "&-roi_0_pred"] = 0.15
|
||||
---
|
||||
> if adx_value > thresholds["adx_trend"]: # 强趋势市场
|
||||
> dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"] # 高ROI
|
||||
> elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
|
||||
> dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"] # 低ROI
|
||||
> else: # 中间状态
|
||||
> dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"] # 中等ROI
|
||||
> # 风险控制:设置ROI上限
|
||||
> if dataframe.at[index, "&-roi_0_pred"] > 0.10:
|
||||
> dataframe.at[index, "&-roi_0_pred"] = 0.10
|
||||
273c240
|
||||
< # RSI预测
|
||||
---
|
||||
> # 计算RSI预测(现有逻辑)
|
||||
278c245
|
||||
< dataframe = dataframe.ffill().fillna(0)
|
||||
---
|
||||
> dataframe = dataframe.fillna(method="ffill").fillna(0)
|
||||
295,304d261
|
||||
< # Ensure DatetimeIndex before FreqAI
|
||||
< if not isinstance(dataframe.index, pd.DatetimeIndex):
|
||||
< logger.warning("Index is not DatetimeIndex in populate_indicators")
|
||||
< if 'date' in dataframe.columns:
|
||||
< dataframe.set_index(pd.to_datetime(dataframe['date']), inplace=True)
|
||||
< dataframe.drop(columns=['date'], inplace=True)
|
||||
< else:
|
||||
< logger.error("No 'date' column and index is not DatetimeIndex")
|
||||
< raise ValueError("Invalid DataFrame index")
|
||||
<
|
||||
306d262
|
||||
< logger.debug(f"DataFrame columns before FreqAI: {list(dataframe.columns)}")
|
||||
314,321c270,273
|
||||
< # 预测统计
|
||||
< if "&-s_close" in dataframe.columns:
|
||||
< logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, "
|
||||
< f"方差={dataframe['&-s_close'].var():.4f}")
|
||||
<
|
||||
< # 添加ATR指标
|
||||
< atr_col = f'ATR_{self.atr_period.value}'
|
||||
< dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
|
||||
---
|
||||
> # 预测统计
|
||||
> if "&-s_close" in dataframe.columns:
|
||||
> logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, "
|
||||
> f"方差={dataframe['&-s_close'].var():.4f}")
|
||||
323,324c275,276
|
||||
< logger.debug(f"生成的列:{list(dataframe.columns)}")
|
||||
< return dataframe
|
||||
---
|
||||
> logger.debug(f"生成的列:{list(dataframe.columns)}")
|
||||
> return dataframe
|
||||
330a283,289
|
||||
> # 确保返回 DataFrame,防止 None
|
||||
> if dataframe is None:
|
||||
> dataframe = DataFrame()
|
||||
> dataframe['ATR_{}'.format(self.atr_period.value)] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
|
||||
> return dataframe
|
||||
>
|
||||
>
|
||||
334a294
|
||||
> # 确保 "%-%-rsi-14" 列存在
|
||||
340,342c300,302
|
||||
< (dataframe["&-s_close"] > 0.01) &
|
||||
< (dataframe["do_predict"] == 1) &
|
||||
< (dataframe["%-%-rsi-14"] < dataframe["&-buy_rsi_pred"])
|
||||
---
|
||||
> (dataframe["&-s_close"] > 0.01) & # 预测价格上涨 > 1%
|
||||
> (dataframe["do_predict"] == 1) & # 预测可靠
|
||||
> (dataframe["%-%-rsi-14"] < dataframe["&-buy_rsi_pred"]) # RSI 低于动态阈值
|
||||
345a306
|
||||
> # 设置 entry_price 列,用于止损逻辑
|
||||
350,351c311
|
||||
<
|
||||
< def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.5) -> DataFrame:
|
||||
---
|
||||
> def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
|
||||
353a314,318
|
||||
> :param dataframe: 原始DataFrame
|
||||
> :param metadata: 策略元数据
|
||||
> :param atr_col: 使用的ATR列名
|
||||
> :param multiplier: ATR乘数
|
||||
> :return: 更新后的DataFrame
|
||||
360c325,326
|
||||
< 'exit_long'] = 1
|
||||
---
|
||||
> 'exit_long'
|
||||
> ] = 1
|
||||
363d328
|
||||
<
|
||||
365,373c330,332
|
||||
< """
|
||||
< 基于动态止损和ROI预测生成退出信号。
|
||||
< """
|
||||
< atr_col = f'ATR_{self.atr_period.value}'
|
||||
< if atr_col not in dataframe.columns:
|
||||
< dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
|
||||
<
|
||||
< dataframe['stop_loss_line'] = dataframe['entry_price'] - (dataframe[atr_col] * self.atr_multiplier.value)
|
||||
< dataframe['roi_target'] = dataframe['entry_price'] * (1 + dataframe['&-roi_0_pred'])
|
||||
---
|
||||
> # 确保 ATR 列存在
|
||||
> if 'ATR_14' not in dataframe.columns:
|
||||
> dataframe['ATR_14'] = 0.0
|
||||
375,380c334,335
|
||||
< dataframe.loc[
|
||||
< (
|
||||
< (dataframe['close'] <= dataframe['stop_loss_line']) |
|
||||
< (dataframe['close'] >= dataframe['roi_target'])
|
||||
< ),
|
||||
< 'exit_long'] = 1
|
||||
---
|
||||
> # 计算动态止损线
|
||||
> dataframe['stop_loss_line'] = dataframe['entry_price'] - (dataframe['ATR_14'] * 2)
|
||||
382,383c337,338
|
||||
< if dataframe['exit_long'].iloc[-1] == 1:
|
||||
< self.dp.send_msg(f"ATR: {dataframe[atr_col].iloc[-1]:.5f}, Stop Loss Line: {dataframe['stop_loss_line'].iloc[-1]:.5f}, ROI Target: {dataframe['roi_target'].iloc[-1]:.5f}")
|
||||
---
|
||||
> # 发送止损信息
|
||||
> self.dp.send_msg(f"ATR: {dataframe['ATR_14'].iloc[-1]:.5f}, Stop Loss Line: {dataframe['stop_loss_line'].iloc[-1]:.5f}")
|
||||
385,386c340,341
|
||||
< logger.debug(f"生成 {dataframe['exit_long'].sum()} 个退出信号")
|
||||
< return dataframe
|
||||
---
|
||||
> # 应用动态止损逻辑
|
||||
> return self._dynamic_stop_loss(dataframe, metadata)
|
||||
397d351
|
||||
<
|
||||
401,403c355
|
||||
< """
|
||||
< 自定义静态止损,基于ATR。
|
||||
< """
|
||||
---
|
||||
>
|
||||
405,408c357,359
|
||||
< atr_col = f'ATR_{self.atr_period.value}'
|
||||
< atr_value = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)[atr_col].iloc[-1]
|
||||
< trailing_stop = current_rate - atr_value * 2.0
|
||||
< return trailing_stop / current_rate - 1
|
||||
---
|
||||
> atr_value = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)['ATR_14'].iloc[-1]
|
||||
> trailing_stop = current_rate - atr_value * 1.5
|
||||
> return trailing_stop / current_rate - 1 # 返回相对百分比
|
||||
432a384
|
||||
> # 假设数据总是新鲜的(用于测试)
|
||||
437c389
|
||||
< 获取 OKX 订单簿数据,带重试和缓存。
|
||||
---
|
||||
> 获取 OKX 订单簿数据。
|
||||
439,464c391,399
|
||||
< cache_key = f"{pair}_{limit}"
|
||||
< cache_timeout = 60 # Cache for 60 seconds
|
||||
< if cache_key in self.order_book_cache:
|
||||
< cached_time, cached_data = self.order_book_cache[cache_key]
|
||||
< if time.time() - cached_time < cache_timeout:
|
||||
< logger.debug(f"Using cached order book for {pair}")
|
||||
< return cached_data
|
||||
<
|
||||
< max_retries = 3
|
||||
< for attempt in range(max_retries):
|
||||
< try:
|
||||
< exchange = ccxt.okx(self.config["exchange"]["ccxt_config"])
|
||||
< order_book = exchange.fetch_order_book(pair, limit=limit)
|
||||
< bids = sum([bid[1] for bid in order_book["bids"]])
|
||||
< asks = sum([ask[1] for ask in order_book["asks"]])
|
||||
< result = {"bids": bids, "asks": asks}
|
||||
< # Cache result
|
||||
< self.order_book_cache[cache_key] = (time.time(), result)
|
||||
< return result
|
||||
< except Exception as e:
|
||||
< logger.warning(f"Attempt {attempt + 1}/{max_retries} failed for {pair}: {str(e)}")
|
||||
< if attempt < max_retries - 1:
|
||||
< time.sleep(1) # Wait before retry
|
||||
< else:
|
||||
< logger.error(f"获取 {pair} 订单簿失败 after {max_retries} attempts: {str(e)}")
|
||||
< return {"bids": 0, "asks": 0}
|
||||
---
|
||||
> try:
|
||||
> exchange = ccxt.okx(self.config["exchange"]["ccxt_config"])
|
||||
> order_book = exchange.fetch_order_book(pair, limit=limit)
|
||||
> bids = sum([bid[1] for bid in order_book["bids"]])
|
||||
> asks = sum([ask[1] for ask in order_book["asks"]])
|
||||
> return {"bids": bids, "asks": asks}
|
||||
> except Exception as e:
|
||||
> logger.error(f"获取 {pair} 订单簿失败:{str(e)}")
|
||||
> return {"bids": 0, "asks": 0}
|
||||
470a406
|
||||
> # 初始化模型
|
||||
475a412
|
||||
> # 调用 FreqAI 训练
|
||||
477a415
|
||||
> # 记录训练集性能
|
||||
483a422
|
||||
> # 记录测试集性能(如果可用)
|
||||
490a430
|
||||
> # 特征重要性
|
||||
@ -1,584 +0,0 @@
|
||||
import logging
|
||||
from freqtrade.strategy import IStrategy
|
||||
from pandas import DataFrame
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import talib as ta
|
||||
import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from sklearn.metrics import mean_squared_error
|
||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter
|
||||
from xgboost import XGBRegressor
|
||||
import ccxt
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OKXRegressionStrategy(IStrategy):
|
||||
"""
|
||||
Freqtrade AI 策略,使用回归模型进行 OKX 数据上的仅做多交易。
|
||||
- 数据通过 CCXT 从 OKX 交易所获取。
|
||||
- 使用 XGBoost 回归模型预测价格变化。
|
||||
- 仅生成做多(买入)信号,不做空。
|
||||
- 适配 Freqtrade 2025.3,继承 IStrategy。
|
||||
"""
|
||||
|
||||
# 指标所需的最大启动蜡烛数
|
||||
startup_candle_count: int = 20
|
||||
|
||||
# 策略元数据(建议通过 config.json 配置)
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.07
|
||||
max_open_trades = 3
|
||||
stake_amount = 'dynamic'
|
||||
atr_period = CategoricalParameter([7, 14, 21], default=14, space='buy')
|
||||
atr_multiplier = DecimalParameter(1.0, 3.0, default=2.0, space='sell')
|
||||
|
||||
# FreqAI 配置
|
||||
freqai_config = {
|
||||
"enabled": True,
|
||||
"identifier": "okx_regression_v1",
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 100,
|
||||
"learning_rate": 0.05,
|
||||
"max_depth": 6
|
||||
},
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["5m", "15m", "1h"],
|
||||
"include_corr_pairlist": ["BTC/USDT", "ETH/USDT"],
|
||||
"label_period_candles": 12,
|
||||
"include_shifted_candles": 2, # 添加历史偏移特征
|
||||
"principal_component_analysis": True # 启用 PCA
|
||||
},
|
||||
"data_split_parameters": {
|
||||
"test_size": 0.2,
|
||||
"random_state": 42,
|
||||
"shuffle": False
|
||||
},
|
||||
"train_period_days": 90,
|
||||
"backtest_period_days": 30,
|
||||
"purge_old_models": True # 清理旧模型
|
||||
}
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
super().__init__(config)
|
||||
# 初始化特征缓存
|
||||
self.feature_cache = {}
|
||||
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: Dict, **kwargs) -> DataFrame:
|
||||
"""
|
||||
为每个时间框架和相关交易对生成特征。
|
||||
"""
|
||||
cache_key = f"{metadata.get('pair', 'unknown')}_{period}"
|
||||
if cache_key in self.feature_cache:
|
||||
logger.debug(f"使用缓存特征:{cache_key}")
|
||||
return self.feature_cache[cache_key]
|
||||
|
||||
# RSI
|
||||
dataframe[f"%-%-rsi-{period}"] = ta.RSI(dataframe["close"], timeperiod=period)
|
||||
|
||||
# MACD
|
||||
macd, macdsignal, _ = ta.MACD(dataframe["close"], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe[f"%-%-macd-{period}"] = macd
|
||||
dataframe[f"%-%-macdsignal-{period}"] = macdsignal
|
||||
|
||||
# 布林带宽度
|
||||
upper, middle, lower = ta.BBANDS(dataframe["close"], timeperiod=period)
|
||||
dataframe[f"%-%-bb_width-{period}"] = (upper - lower) / middle
|
||||
|
||||
# 成交量均线
|
||||
dataframe[f"%-%-volume_ma-{period}"] = ta.SMA(dataframe["volume"], timeperiod=period)
|
||||
|
||||
# 仅为 BTC/USDT 和 ETH/USDT 生成 order_book_imbalance
|
||||
#
|
||||
pair = metadata.get('pair', 'unknown')
|
||||
# 注释掉订单簿相关代码
|
||||
# if pair in ["BTC/USDT", "ETH/USDT"]:
|
||||
# try:
|
||||
# order_book = self.fetch_okx_order_book(pair)
|
||||
# dataframe[f"%-%-order_book_imbalance"] = (
|
||||
# order_book["bids"] - order_book["asks"]
|
||||
# ) / (order_book["bids"] + order_book["asks"] + 1e-10)
|
||||
# except Exception as e:
|
||||
# logger.warning(f"Failed to fetch order book for {pair}: {str(e)}")
|
||||
# dataframe[f"%-%-order_book_imbalance"] = 0.0
|
||||
|
||||
# 数据清洗
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
|
||||
dataframe = dataframe.ffill().fillna(0)
|
||||
|
||||
# 缓存特征
|
||||
self.feature_cache[cache_key] = dataframe.copy()
|
||||
logger.debug(f"周期 {period} 特征:{list(dataframe.filter(like='%-%-').columns)}")
|
||||
|
||||
return dataframe
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||
"""
|
||||
添加基础时间框架的全局特征。
|
||||
"""
|
||||
# 确保索引是 DatetimeIndex
|
||||
if not isinstance(dataframe.index, pd.DatetimeIndex):
|
||||
dataframe = dataframe.set_index(pd.DatetimeIndex(dataframe.index))
|
||||
|
||||
# 价格变化率
|
||||
dataframe["%-price_change"] = dataframe["close"].pct_change()
|
||||
|
||||
# 时间特征:小时
|
||||
dataframe["%-hour_of_day"] = dataframe.index.hour / 24.0
|
||||
|
||||
# 数据清洗
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
|
||||
dataframe = dataframe.ffill().fillna(0)
|
||||
|
||||
logger.debug(f"全局特征:{list(dataframe.filter(like='%-%-').columns)}")
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
|
||||
"""
|
||||
设置回归模型的目标变量,为不同币对设置动态止损和ROI阈值。
|
||||
输入:dataframe(K线数据:close, high, low),metadata(交易对信息),config(FreqAI配置)
|
||||
输出:更新后的dataframe,包含目标标签
|
||||
"""
|
||||
# 获取配置参数
|
||||
label_period = self.freqai_config["feature_parameters"]["label_period_candles"] # 标签预测周期(如5分钟K线的N根)
|
||||
pair = metadata["pair"] # 当前交易对(如DOGE/USDT)
|
||||
|
||||
# 计算未来价格变化率(现有逻辑)
|
||||
dataframe["&-s_close"] = (dataframe["close"].shift(-label_period) - dataframe["close"]) / dataframe["close"]
|
||||
|
||||
# 计算不同时间窗口的ROI(现有逻辑)
|
||||
for minutes in [0, 15, 30]:
|
||||
candles = int(minutes / 5) # 假设5分钟K线
|
||||
if candles > 0:
|
||||
dataframe[f"&-roi_{minutes}"] = (dataframe["close"].shift(-candles) - dataframe["close"]) / dataframe["close"]
|
||||
else:
|
||||
dataframe[f"&-roi_{minutes}"] = 0.0
|
||||
|
||||
# 计算市场状态指标:ADX(14周期,与label_period_candles对齐)
|
||||
dataframe["adx"] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
|
||||
|
||||
# 定义币对特定的ADX阈值和止损/ROI范围
|
||||
pair_thresholds = {
|
||||
"BTC/USDT": {
|
||||
"adx_trend": 25,
|
||||
"adx_oscillation": 20,
|
||||
"stoploss_trend": -0.03,
|
||||
"stoploss_oscillation": -0.015,
|
||||
"stoploss_mid": -0.02,
|
||||
"roi_trend": 0.03,
|
||||
"roi_oscillation": 0.015,
|
||||
"roi_mid": 0.02
|
||||
},
|
||||
"SOL/USDT": {
|
||||
"adx_trend": 22,
|
||||
"adx_oscillation": 18,
|
||||
"stoploss_trend": -0.06,
|
||||
"stoploss_oscillation": -0.03,
|
||||
"stoploss_mid": -0.045,
|
||||
"roi_trend": 0.045,
|
||||
"roi_oscillation": 0.02,
|
||||
"roi_mid": 0.03
|
||||
},
|
||||
"XRP/USDT": {
|
||||
"adx_trend": 22,
|
||||
"adx_oscillation": 18,
|
||||
"stoploss_trend": -0.06,
|
||||
"stoploss_oscillation": -0.03,
|
||||
"stoploss_mid": -0.045,
|
||||
"roi_trend": 0.045,
|
||||
"roi_oscillation": 0.02,
|
||||
"roi_mid": 0.03
|
||||
},
|
||||
"XRP/USDT": {
|
||||
"adx_trend": 22,
|
||||
"adx_oscillation": 18,
|
||||
"stoploss_trend": -0.06,
|
||||
"stoploss_oscillation": -0.03,
|
||||
"stoploss_mid": -0.045,
|
||||
"roi_trend": 0.045,
|
||||
"roi_oscillation": 0.02,
|
||||
"roi_mid": 0.03
|
||||
},
|
||||
"OKB/USDT": {
|
||||
"adx_trend": 36, # 放松趋势识别,更低的 ADX 阈值 (原值 x2)
|
||||
"adx_oscillation": 24, # 更低的震荡识别阈值 (原值 x2)
|
||||
"stoploss_trend": -0.20, # 更宽松的趋势止损 (原值 x2)
|
||||
"stoploss_oscillation": -0.12, # 更宽松的震荡止损 (原值 x2)
|
||||
"stoploss_mid": -0.16, # 中间状态止损也放宽 (原值 x2)
|
||||
"roi_trend": 0.14, # 提高趋势 ROI 目标 (原值 x2)
|
||||
"roi_oscillation": 0.08, # 提高震荡 ROI 目标 (原值 x2)
|
||||
"roi_mid": 0.10 # 中间状态 ROI 适度提高 (原值 x2)
|
||||
}
|
||||
}
|
||||
|
||||
# 动态化 &-stoploss_pred(基于市场状态和币对)
|
||||
dataframe["&-stoploss_pred"] = 0.0
|
||||
for index, row in dataframe.iterrows():
|
||||
thresholds = pair_thresholds.get(pair, {})
|
||||
if not thresholds:
|
||||
continue
|
||||
adx_value = row["adx"]
|
||||
if adx_value > thresholds["adx_trend"]: # 趋势市场
|
||||
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_trend"] # 宽松止损
|
||||
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
|
||||
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_oscillation"] # 严格止损
|
||||
else: # 中间状态
|
||||
dataframe.at[index, "&-stoploss_pred"] = thresholds["stoploss_mid"] # 中等止损
|
||||
# 风险控制:设置止损下限
|
||||
if dataframe.at[index, "&-stoploss_pred"] < -0.10:
|
||||
dataframe.at[index, "&-stoploss_pred"] = -0.10
|
||||
|
||||
# 动态化 &-roi_0_pred(基于市场趋势和币对)
|
||||
dataframe["&-roi_0_pred"] = 0.0
|
||||
for index, row in dataframe.iterrows():
|
||||
thresholds = pair_thresholds.get(pair, {})
|
||||
if not thresholds:
|
||||
continue
|
||||
adx_value = row["adx"]
|
||||
if adx_value > thresholds["adx_trend"]: # 强趋势市场
|
||||
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_trend"] # 高ROI
|
||||
elif adx_value < thresholds["adx_oscillation"]: # 震荡市场
|
||||
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_oscillation"] # 低ROI
|
||||
else: # 中间状态
|
||||
dataframe.at[index, "&-roi_0_pred"] = thresholds["roi_mid"] # 中等ROI
|
||||
# 风险控制:设置ROI上限
|
||||
if dataframe.at[index, "&-roi_0_pred"] > 0.10:
|
||||
dataframe.at[index, "&-roi_0_pred"] = 0.10
|
||||
|
||||
# 计算RSI预测(现有逻辑)
|
||||
dataframe["&-buy_rsi_pred"] = ta.RSI(dataframe["close"], timeperiod=14).rolling(20).mean()
|
||||
|
||||
# 数据清洗
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
|
||||
#dataframe = dataframe.fillna(method="ffill").fillna(0)
|
||||
dataframe = dataframe.ffill()
|
||||
|
||||
# 验证目标
|
||||
required_targets = ["&-s_close", "&-roi_0", "&-buy_rsi_pred", "&-stoploss_pred", "&-roi_0_pred"]
|
||||
missing_targets = [col for col in required_targets if col not in dataframe.columns]
|
||||
if missing_targets:
|
||||
logger.error(f"缺少目标列:{missing_targets}")
|
||||
raise ValueError(f"目标初始化失败:{missing_targets}")
|
||||
|
||||
logger.debug(f"目标初始化完成。DataFrame 形状:{dataframe.shape}")
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
|
||||
"""
|
||||
使用 FreqAI 生成指标和预测。
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"FreqAI 对象:{type(self.freqai)}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
# 验证数据完整性
|
||||
if dataframe["close"].isna().any() or dataframe["volume"].isna().any():
|
||||
logger.warning("检测到 OKX 数据缺失,使用前向填充")
|
||||
dataframe = dataframe.ffill().fillna(0)
|
||||
|
||||
# 预测统计
|
||||
if "&-s_close" in dataframe.columns:
|
||||
logger.debug(f"预测统计:均值={dataframe['&-s_close'].mean():.4f}, "
|
||||
f"方差={dataframe['&-s_close'].var():.4f}")
|
||||
|
||||
logger.debug(f"生成的列:{list(dataframe.columns)}")
|
||||
return dataframe
|
||||
except Exception as e:
|
||||
logger.error(f"FreqAI start 失败:{str(e)}")
|
||||
raise
|
||||
finally:
|
||||
logger.debug("populate_indicators 完成")
|
||||
|
||||
# 确保返回 DataFrame,防止 None
|
||||
if dataframe is None:
|
||||
dataframe = DataFrame()
|
||||
dataframe['ATR_{}'.format(self.atr_period.value)] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=self.atr_period.value)
|
||||
# 添加 RSI 和布林带指标
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
upper, middle, lower = ta.BBANDS(dataframe['close'], timeperiod=20, nbdevup=2, nbdevdn=2)
|
||||
dataframe['bb_upper'] = upper
|
||||
dataframe['bb_middle'] = middle
|
||||
dataframe['bb_lower'] = lower
|
||||
|
||||
# 添加高时间框架数据(例如 1h)
|
||||
dataframe_1h = None # 显式声明
|
||||
if self.config['timeframe'] != '1h':
|
||||
dataframe_1h = self.dp.get_analyzed_dataframe(metadata['pair'], '1h')[0]
|
||||
# 确保 dataframe_1h 存在 'close' 列并且非空
|
||||
if dataframe_1h is not None and not dataframe_1h.empty and 'close' in dataframe_1h.columns:
|
||||
dataframe['trend_1h'] = dataframe_1h['close'].rolling(window=20).mean()
|
||||
else:
|
||||
# 回退到当前时间框架数据
|
||||
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
|
||||
else:
|
||||
dataframe['trend_1h'] = dataframe['close'].rolling(window=20).mean()
|
||||
|
||||
# 记录 1h 数据框信息
|
||||
logger.debug(f"dataframe_1h columns: {dataframe_1h.columns.tolist() if dataframe_1h is not None and not dataframe_1h.empty else '未使用'}")
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
|
||||
"""
|
||||
使用模型预测置信度进行入场过滤:
|
||||
- 仅当 `pred_upper - pred_lower > 0.02` 时允许开仓
|
||||
"""
|
||||
if "&-s_close" in dataframe.columns and "pred_upper" in dataframe.columns and "pred_lower" in dataframe.columns:
|
||||
# 计算置信区间宽度
|
||||
dataframe["confidence_width"] = dataframe["pred_upper"] - dataframe["pred_lower"]
|
||||
|
||||
# 应用置信度过滤(仅在宽度 > 0.02 时允许交易)
|
||||
dataframe.loc[
|
||||
(dataframe["confidence_width"] > 0.02),
|
||||
"enter_long"
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
# 已废弃:使用 custom_stoploss + ATR 百分位替代
|
||||
# def _dynamic_stop_loss(self, dataframe: DataFrame, metadata: dict, atr_col: str = 'ATR_14', multiplier: float = 2.0) -> DataFrame:
|
||||
# ...
|
||||
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
|
||||
|
||||
# 根据波动率百分位调整止盈倍数
|
||||
if percentile > 80: # 高波动市场,缩短止盈距离
|
||||
volatility_adjustment = 0.8
|
||||
elif percentile < 20: # 低波动市场,拉长止盈距离
|
||||
volatility_adjustment = 1.2
|
||||
else: # 正常波动市场,保持默认
|
||||
volatility_adjustment = 1.0
|
||||
|
||||
# 获取ADX指标判断趋势强度
|
||||
dataframe['adx'] = ta.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
|
||||
adx_value = dataframe['adx'].iloc[-1]
|
||||
|
||||
# 根据ADX趋势强度调整止盈倍数
|
||||
if adx_value > 25: # 强趋势,延长止盈距离
|
||||
trend_adjustment = 1.3
|
||||
elif adx_value < 15: # 震荡行情,缩短止盈距离
|
||||
trend_adjustment = 0.7
|
||||
else: # 中性趋势,保持默认
|
||||
trend_adjustment = 1.0
|
||||
|
||||
# 综合调整止盈倍数
|
||||
adjusted_multiplier = take_profit_multiplier * volatility_adjustment * trend_adjustment
|
||||
|
||||
# 计算止盈线
|
||||
dataframe['entry_price'] = dataframe['open'].where(dataframe['enter_long'] == 1).ffill()
|
||||
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe[atr_col] * adjusted_multiplier
|
||||
|
||||
# 应用止盈逻辑
|
||||
dataframe.loc[
|
||||
(dataframe['close'] > dataframe['take_profit_line']),
|
||||
'exit_long'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame:
|
||||
"""
|
||||
当前退出逻辑已由 custom_stoploss 和 callback_stop_loss 管理。
|
||||
此方法保留为空以维持向后兼容性。
|
||||
"""
|
||||
return dataframe
|
||||
|
||||
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
"""
|
||||
动态下注:每笔交易占账户余额 2%。
|
||||
"""
|
||||
balance = self.wallets.get_available_stake_amount()
|
||||
stake = balance * 0.02
|
||||
return min(max(stake, min_stake), max_stake)
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, profit_percent: float,
|
||||
after_fill: bool, **kwargs) -> Optional[float]:
|
||||
"""
|
||||
自适应止损:基于市场波动率百分位动态调整ATR乘数
|
||||
"""
|
||||
if trade.enter_tag == 'long':
|
||||
# 获取多个周期的ATR值
|
||||
dataframe = self.dp.get_pair_dataframe(pair, timeframe=self.timeframe)
|
||||
|
||||
# 计算不同周期的ATR
|
||||
dataframe['ATR_7'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=7)
|
||||
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
|
||||
dataframe['ATR_21'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=21)
|
||||
|
||||
# 计算20日平均ATR(用于百分位计算)
|
||||
avg_atr_window = 20
|
||||
dataframe['avg_atr'] = dataframe['ATR_14'].rolling(window=avg_atr_window).mean()
|
||||
|
||||
# 根据交易对调整基础ATR值
|
||||
pair_specific_atr = {
|
||||
"BTC/USDT": latest_row['ATR_14'],
|
||||
"ETH/USDT": latest_row['ATR_14'],
|
||||
"OKB/USDT": latest_row['ATR_14'], # 使用更长周期 ATR 减少波动影响
|
||||
"TON/USDT": latest_row['ATR_7']
|
||||
}
|
||||
|
||||
current_atr = latest_row['avg_atr']
|
||||
percentile = (np.sum(historical_atr < current_atr) / len(historical_atr)) * 100
|
||||
|
||||
# 根据市场波动率百分位选择ATR乘数
|
||||
if percentile > 80: # 高波动市场
|
||||
atr_multiplier = 1.5
|
||||
elif percentile < 20: # 低波动市场
|
||||
atr_multiplier = 2.5
|
||||
else: # 正常波动市场
|
||||
atr_multiplier = 2.0
|
||||
|
||||
# 根据交易对调整基础ATR值
|
||||
pair_specific_atr = {
|
||||
"BTC/USDT": latest_row['ATR_14'],
|
||||
"ETH/USDT": latest_row['ATR_14'],
|
||||
"OKB/USDT": latest_row['ATR_14'], # 使用更长周期 ATR 减少波动影响
|
||||
"TON/USDT": latest_row['ATR_7']
|
||||
}
|
||||
|
||||
if pair in pair_specific_atr:
|
||||
base_atr = pair_specific_atr[pair]
|
||||
else:
|
||||
base_atr = latest_row['ATR_14']
|
||||
|
||||
# 计算追踪止损价格
|
||||
trailing_stop = current_rate - base_atr * atr_multiplier
|
||||
|
||||
# 添加额外条件:确保止损不低于入场价的一定比例
|
||||
min_profit_ratio = 0.005 # 最低盈利0.5%
|
||||
min_stop_price = trade.open_rate * (1 + min_profit_ratio)
|
||||
final_stop_price = max(trailing_stop, min_stop_price)
|
||||
|
||||
return final_stop_price / current_rate - 1 # 返回相对百分比
|
||||
|
||||
return None
|
||||
|
||||
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, side: str,
|
||||
**kwargs) -> float:
|
||||
"""
|
||||
禁用杠杆,仅做多。
|
||||
"""
|
||||
return 1.0
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: 'datetime', **kwargs) -> bool:
|
||||
"""
|
||||
入场确认逻辑已被禁用以简化流程。
|
||||
"""
|
||||
return True
|
||||
|
||||
def check_data_freshness(self, pair: str, current_time: 'datetime') -> bool:
|
||||
"""
|
||||
数据新鲜度检查已被禁用以简化逻辑。
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def fit(self, data_dictionary: Dict, metadata: Dict, **kwargs) -> None:
|
||||
"""
|
||||
训练回归模型并记录性能。
|
||||
"""
|
||||
try:
|
||||
# 初始化模型
|
||||
if not hasattr(self, 'model') or self.model is None:
|
||||
model_params = self.freqai_config["model_training_parameters"]
|
||||
self.model = XGBRegressor(**model_params)
|
||||
logger.debug("初始化新的 XGBoost 回归模型")
|
||||
|
||||
# 调用 FreqAI 训练
|
||||
self.freqai.fit(data_dictionary, metadata, **kwargs)
|
||||
|
||||
# 记录训练集性能
|
||||
train_data = data_dictionary["train_features"]
|
||||
train_labels = data_dictionary["train_labels"]
|
||||
train_predictions = self.model.predict(train_data)
|
||||
train_mse = mean_squared_error(train_labels, train_predictions)
|
||||
logger.info(f"训练集 MSE:{train_mse:.6f}")
|
||||
|
||||
# 记录测试集性能(如果可用)
|
||||
if "test_features" in data_dictionary:
|
||||
test_data = data_dictionary["test_features"]
|
||||
test_labels = data_dictionary["test_labels"]
|
||||
test_predictions = self.model.predict(test_data)
|
||||
test_mse = mean_squared_error(test_labels, test_predictions)
|
||||
logger.info(f"测试集 MSE:{test_mse:.6f}")
|
||||
|
||||
# 特征重要性
|
||||
if hasattr(self.model, 'feature_importances_'):
|
||||
importance = self.model.feature_importances_
|
||||
logger.debug(f"特征重要性:{dict(zip(train_data.columns, importance))}")
|
||||
except Exception as e:
|
||||
logger.error(f"FreqAI fit 失败:{str(e)}")
|
||||
raise
|
||||
def _callback_stop_loss(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
动态回调止损逻辑:基于ATR调整回撤阈值,并结合RSI和布林带过滤信号。
|
||||
"""
|
||||
pair = metadata.get('pair', 'unknown')
|
||||
|
||||
# 设置默认参数
|
||||
atr_col = 'ATR_14'
|
||||
rolling_high_period = 20
|
||||
rsi_overbought = 70
|
||||
|
||||
# 设置不同币种的回调乘数
|
||||
callback_multipliers = {
|
||||
"BTC/USDT": 1.5,
|
||||
"ETH/USDT": 2.0,
|
||||
"OKB/USDT": 1.3,
|
||||
"TON/USDT": 2.0,
|
||||
}
|
||||
callback_multiplier = callback_multipliers.get(pair, 2.0)
|
||||
|
||||
# 确保ATR列存在
|
||||
if atr_col not in dataframe.columns:
|
||||
dataframe[atr_col] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
|
||||
|
||||
# 计算动态回调百分比(基于ATR)
|
||||
dataframe['callback_threshold'] = dataframe[atr_col] * callback_multiplier
|
||||
|
||||
# 计算ATR
|
||||
if 'ATR_14' not in dataframe.columns:
|
||||
dataframe['ATR_14'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14)
|
||||
|
||||
# 计算最近高点
|
||||
dataframe['rolling_high'] = dataframe['close'].rolling(window=20).max()
|
||||
|
||||
# 计算回调阈值
|
||||
dataframe['take_profit_line'] = dataframe['entry_price'] + dataframe['ATR_14'] * callback_multiplier
|
||||
|
||||
# 应用止盈逻辑
|
||||
dataframe.loc[
|
||||
(dataframe['close'] > dataframe['take_profit_line']),
|
||||
'exit_long'
|
||||
] = 1
|
||||
|
||||
# 计算当前价格相对于最近高点的回撤比例(使用ATR标准化)
|
||||
dataframe['callback_ratio'] = (dataframe['close'] - dataframe['rolling_high']) / dataframe['rolling_high']
|
||||
dataframe['callback_condition_atr'] = (dataframe['close'] - dataframe['rolling_high']) <= -dataframe['callback_threshold']
|
||||
|
||||
# 获取RSI和布林带信息
|
||||
dataframe['in_overbought'] = dataframe['rsi'] > rsi_overbought
|
||||
dataframe['below_bb_upper'] = dataframe['close'] < dataframe['bb_upper']
|
||||
|
||||
# 获取高时间框架趋势(1小时均线)
|
||||
dataframe['trend_up'] = dataframe['close'] > dataframe['trend_1h']
|
||||
dataframe['trend_down'] = dataframe['close'] < dataframe['trend_1h']
|
||||
|
||||
# 综合回调止损条件
|
||||
callback_condition = (
|
||||
dataframe['callback_condition_atr'] &
|
||||
((dataframe['in_overbought'] | (~dataframe['below_bb_upper']))) &
|
||||
dataframe['trend_down']
|
||||
)
|
||||
|
||||
# 应用回调止损逻辑
|
||||
dataframe.loc[callback_condition, 'exit_long'] = 1
|
||||
|
||||
return dataframe
|
||||
@ -1,69 +0,0 @@
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import talib as ta
|
||||
import logging
|
||||
import datetime
|
||||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MyOptimizedStrategy(IStrategy):
|
||||
# --- Hyperoptables ---
|
||||
buy_rsi = IntParameter(20, 50, default=30, space="buy")
|
||||
sell_rsi = IntParameter(50, 80, default=70, space="sell")
|
||||
|
||||
# --- FreqAI 相关 ---
|
||||
minimal_roi = {"0": 0.05, "30": 0.02, "60": 0}
|
||||
stoploss = -0.1
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.03
|
||||
trailing_stop_positive_offset = 0.05
|
||||
|
||||
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
dataframe["up_or_down"] = np.where(
|
||||
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
|
||||
)
|
||||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||
|
||||
# 添加 volume_ma
|
||||
dataframe["volume_ma"] = dataframe["volume"].rolling(window=20).mean()
|
||||
|
||||
# Bollinger Bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
|
||||
dataframe.replace([np.inf, -np.inf], 0, inplace=True)
|
||||
dataframe.ffill(inplace=True)
|
||||
dataframe.fillna(0, inplace=True)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
conditions = [
|
||||
(dataframe['do_predict'] == 1),
|
||||
(dataframe['rsi'] < dataframe['&-buy_rsi_pred']),
|
||||
(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
]
|
||||
dataframe.loc[reduce(np.logical_and, conditions), 'enter_long'] = 1
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
conditions = [
|
||||
(dataframe['do_predict'] == 1),
|
||||
(dataframe['rsi'] > dataframe['&-sell_rsi_pred'])
|
||||
]
|
||||
dataframe.loc[reduce(np.logical_and, conditions), 'exit_long'] = 1
|
||||
return dataframe
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float,
|
||||
current_profit: float, **kwargs) -> float:
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_row = dataframe.iloc[-1]
|
||||
if "&-stoploss" in last_row:
|
||||
return float(last_row["&-stoploss"])
|
||||
return self.stoploss
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
from datetime import datetime
|
||||
from math import exp
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
# Define some constants:
|
||||
|
||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic
|
||||
# to the number of days
|
||||
TARGET_TRADES = 600
|
||||
# This is assumed to be expected avg profit * expected trade count.
|
||||
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
|
||||
# self.expected_max_profit = 3.85
|
||||
# Check that the reported Σ% values do not exceed this!
|
||||
# Note, this is ratio. 3.85 stated above means 385Σ%.
|
||||
EXPECTED_MAX_PROFIT = 3.0
|
||||
|
||||
# max average trade duration in minutes
|
||||
# if eval ends with higher value, we consider it a failed eval
|
||||
MAX_ACCEPTED_TRADE_DURATION = 300
|
||||
|
||||
|
||||
class SampleHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the default loss function for hyperopt
|
||||
This is intended to give you some inspiration for your own loss function.
|
||||
|
||||
The Function needs to return a number (float) - which becomes smaller for better backtest
|
||||
results.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
processed: dict[str, DataFrame],
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
"""
|
||||
total_profit = results["profit_ratio"].sum()
|
||||
trade_duration = results["trade_duration"].mean()
|
||||
|
||||
trade_loss = 1 - 0.25 * exp(-((trade_count - TARGET_TRADES) ** 2) / 10**5.8)
|
||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
|
||||
result = trade_loss + profit_loss + duration_loss
|
||||
return result
|
||||
@ -1,426 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
# flake8: noqa: F401
|
||||
# isort: skip_file
|
||||
# --- Do not remove these imports ---
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pandas import DataFrame
|
||||
from typing import Optional, Union
|
||||
|
||||
from freqtrade.strategy import (
|
||||
IStrategy,
|
||||
Trade,
|
||||
Order,
|
||||
PairLocks,
|
||||
informative, # @informative decorator
|
||||
# Hyperopt Parameters
|
||||
BooleanParameter,
|
||||
CategoricalParameter,
|
||||
DecimalParameter,
|
||||
IntParameter,
|
||||
RealParameter,
|
||||
# timeframe helpers
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_next_date,
|
||||
timeframe_to_prev_date,
|
||||
# Strategy helper functions
|
||||
merge_informative_pair,
|
||||
stoploss_from_absolute,
|
||||
stoploss_from_open,
|
||||
)
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta
|
||||
from technical import qtpylib
|
||||
|
||||
|
||||
# This class is a sample. Feel free to customize it.
|
||||
class SampleStrategy(IStrategy):
|
||||
"""
|
||||
This is a sample strategy to inspire you.
|
||||
More information in https://www.freqtrade.io/en/latest/strategy-customization/
|
||||
|
||||
You can:
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
- Rename the class name (Do not forget to update class_name)
|
||||
- Add any methods you want to build your strategy
|
||||
- Add any lib you need to build your strategy
|
||||
|
||||
You must keep:
|
||||
- the lib in the section "Do not remove these libs"
|
||||
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
|
||||
You should keep:
|
||||
- timeframe, minimal_roi, stoploss, trailing_*
|
||||
"""
|
||||
|
||||
# Strategy interface version - allow new iterations of the strategy interface.
|
||||
# Check the documentation or the Sample strategy to get the latest version.
|
||||
INTERFACE_VERSION = 3
|
||||
|
||||
# Can this strategy go short?
|
||||
can_short: bool = False
|
||||
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi".
|
||||
minimal_roi = {
|
||||
# "120": 0.0, # exit after 120 minutes at break even
|
||||
"60": 0.01,
|
||||
"30": 0.02,
|
||||
"0": 0.04,
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "stoploss".
|
||||
stoploss = -0.10
|
||||
|
||||
# Trailing stoploss
|
||||
trailing_stop = False
|
||||
# trailing_only_offset_is_reached = False
|
||||
# trailing_stop_positive = 0.01
|
||||
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||
|
||||
# Optimal timeframe for the strategy.
|
||||
timeframe = "5m"
|
||||
|
||||
# Run "populate_indicators()" only for new candle.
|
||||
process_only_new_candles = True
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_exit_signal = True
|
||||
exit_profit_only = False
|
||||
ignore_roi_if_entry_signal = False
|
||||
|
||||
# Hyperoptable parameters
|
||||
buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
|
||||
sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True)
|
||||
short_rsi = IntParameter(low=51, high=100, default=70, space="sell", optimize=True, load=True)
|
||||
exit_short_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
|
||||
|
||||
# Number of candles the strategy requires before producing valid signals
|
||||
startup_candle_count: int = 200
|
||||
|
||||
# Optional order type mapping.
|
||||
order_types = {
|
||||
"entry": "limit",
|
||||
"exit": "limit",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": False,
|
||||
}
|
||||
|
||||
# Optional order time in force.
|
||||
order_time_in_force = {"entry": "GTC", "exit": "GTC"}
|
||||
|
||||
plot_config = {
|
||||
"main_plot": {
|
||||
"tema": {},
|
||||
"sar": {"color": "white"},
|
||||
},
|
||||
"subplots": {
|
||||
"MACD": {
|
||||
"macd": {"color": "blue"},
|
||||
"macdsignal": {"color": "orange"},
|
||||
},
|
||||
"RSI": {
|
||||
"rsi": {"color": "red"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def informative_pairs(self):
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
These pair/interval combinations are non-tradeable, unless they are part
|
||||
of the whitelist as well.
|
||||
For more information, please consult the documentation
|
||||
:return: List of tuples in the format (pair, interval)
|
||||
Sample: return [("ETH/USDT", "5m"),
|
||||
("BTC/USDT", "15m"),
|
||||
]
|
||||
"""
|
||||
return []
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
||||
# Momentum Indicators
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe["adx"] = ta.ADX(dataframe)
|
||||
|
||||
# # Plus Directional Indicator / Movement
|
||||
# dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
# dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
|
||||
# # Minus Directional Indicator / Movement
|
||||
# dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
# dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# # Aroon, Aroon Oscillator
|
||||
# aroon = ta.AROON(dataframe)
|
||||
# dataframe['aroonup'] = aroon['aroonup']
|
||||
# dataframe['aroondown'] = aroon['aroondown']
|
||||
# dataframe['aroonosc'] = ta.AROONOSC(dataframe)
|
||||
|
||||
# # Awesome Oscillator
|
||||
# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
|
||||
# # Keltner Channel
|
||||
# keltner = qtpylib.keltner_channel(dataframe)
|
||||
# dataframe["kc_upperband"] = keltner["upper"]
|
||||
# dataframe["kc_lowerband"] = keltner["lower"]
|
||||
# dataframe["kc_middleband"] = keltner["mid"]
|
||||
# dataframe["kc_percent"] = (
|
||||
# (dataframe["close"] - dataframe["kc_lowerband"]) /
|
||||
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"])
|
||||
# )
|
||||
# dataframe["kc_width"] = (
|
||||
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"]
|
||||
# )
|
||||
|
||||
# # Ultimate Oscillator
|
||||
# dataframe['uo'] = ta.ULTOSC(dataframe)
|
||||
|
||||
# # Commodity Channel Index: values [Oversold:-100, Overbought:100]
|
||||
# dataframe['cci'] = ta.CCI(dataframe)
|
||||
|
||||
# RSI
|
||||
dataframe["rsi"] = ta.RSI(dataframe)
|
||||
|
||||
# # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
# rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
|
||||
|
||||
# # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
|
||||
# # Stochastic Slow
|
||||
# stoch = ta.STOCH(dataframe)
|
||||
# dataframe['slowd'] = stoch['slowd']
|
||||
# dataframe['slowk'] = stoch['slowk']
|
||||
|
||||
# Stochastic Fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe["fastd"] = stoch_fast["fastd"]
|
||||
dataframe["fastk"] = stoch_fast["fastk"]
|
||||
|
||||
# # Stochastic RSI
|
||||
# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this.
|
||||
# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results.
|
||||
# stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
# dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
# dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe["macd"] = macd["macd"]
|
||||
dataframe["macdsignal"] = macd["macdsignal"]
|
||||
dataframe["macdhist"] = macd["macdhist"]
|
||||
|
||||
# MFI
|
||||
dataframe["mfi"] = ta.MFI(dataframe)
|
||||
|
||||
# # ROC
|
||||
# dataframe['roc'] = ta.ROC(dataframe)
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Bollinger Bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||||
dataframe["bb_middleband"] = bollinger["mid"]
|
||||
dataframe["bb_upperband"] = bollinger["upper"]
|
||||
dataframe["bb_percent"] = (dataframe["close"] - dataframe["bb_lowerband"]) / (
|
||||
dataframe["bb_upperband"] - dataframe["bb_lowerband"]
|
||||
)
|
||||
dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe[
|
||||
"bb_middleband"
|
||||
]
|
||||
|
||||
# Bollinger Bands - Weighted (EMA based instead of SMA)
|
||||
# weighted_bollinger = qtpylib.weighted_bollinger_bands(
|
||||
# qtpylib.typical_price(dataframe), window=20, stds=2
|
||||
# )
|
||||
# dataframe["wbb_upperband"] = weighted_bollinger["upper"]
|
||||
# dataframe["wbb_lowerband"] = weighted_bollinger["lower"]
|
||||
# dataframe["wbb_middleband"] = weighted_bollinger["mid"]
|
||||
# dataframe["wbb_percent"] = (
|
||||
# (dataframe["close"] - dataframe["wbb_lowerband"]) /
|
||||
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"])
|
||||
# )
|
||||
# dataframe["wbb_width"] = (
|
||||
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) /
|
||||
# dataframe["wbb_middleband"]
|
||||
# )
|
||||
|
||||
# # EMA - Exponential Moving Average
|
||||
# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
# dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
|
||||
# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# # SMA - Simple Moving Average
|
||||
# dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3)
|
||||
# dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5)
|
||||
# dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10)
|
||||
# dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21)
|
||||
# dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50)
|
||||
# dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100)
|
||||
|
||||
# Parabolic SAR
|
||||
dataframe["sar"] = ta.SAR(dataframe)
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe["htsine"] = hilbert["sine"]
|
||||
dataframe["htleadsine"] = hilbert["leadsine"]
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
# # Hammer: values [0, 100]
|
||||
# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# # Inverted Hammer: values [0, 100]
|
||||
# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# # Dragonfly Doji: values [0, 100]
|
||||
# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# # Piercing Line: values [0, 100]
|
||||
# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# # Morningstar: values [0, 100]
|
||||
# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# # Three White Soldiers: values [0, 100]
|
||||
# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
# # Hanging Man: values [0, 100]
|
||||
# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# # Shooting Star: values [0, 100]
|
||||
# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# # Gravestone Doji: values [0, 100]
|
||||
# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# # Dark Cloud Cover: values [0, 100]
|
||||
# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# # Evening Doji Star: values [0, 100]
|
||||
# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# # Evening Star: values [0, 100]
|
||||
# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
# # Three Line Strike: values [0, -100, 100]
|
||||
# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# # Spinning Top: values [0, -100, 100]
|
||||
# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# # Engulfing: values [0, -100, 100]
|
||||
# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# # Harami: values [0, -100, 100]
|
||||
# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# # Three Outside Up/Down: values [0, -100, 100]
|
||||
# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# # Three Inside Up/Down: values [0, -100, 100]
|
||||
# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
|
||||
# # Chart type
|
||||
# # ------------------------------------
|
||||
# # Heikin Ashi Strategy
|
||||
# heikinashi = qtpylib.heikinashi(dataframe)
|
||||
# dataframe['ha_open'] = heikinashi['open']
|
||||
# dataframe['ha_close'] = heikinashi['close']
|
||||
# dataframe['ha_high'] = heikinashi['high']
|
||||
# dataframe['ha_low'] = heikinashi['low']
|
||||
|
||||
# Retrieve best bid and best ask from the orderbook
|
||||
# ------------------------------------
|
||||
"""
|
||||
# first check if dataprovider is available
|
||||
if self.dp:
|
||||
if self.dp.runmode.value in ('live', 'dry_run'):
|
||||
ob = self.dp.orderbook(metadata['pair'], 1)
|
||||
dataframe['best_bid'] = ob['bids'][0][0]
|
||||
dataframe['best_ask'] = ob['asks'][0][0]
|
||||
"""
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the entry signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with entry columns populated
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
# Signal: RSI crosses above 30
|
||||
(qtpylib.crossed_above(dataframe["rsi"], self.buy_rsi.value))
|
||||
& (dataframe["tema"] <= dataframe["bb_middleband"]) # Guard: tema below BB middle
|
||||
& (dataframe["tema"] > dataframe["tema"].shift(1)) # Guard: tema is raising
|
||||
& (dataframe["volume"] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
"enter_long",
|
||||
] = 1
|
||||
|
||||
dataframe.loc[
|
||||
(
|
||||
# Signal: RSI crosses above 70
|
||||
(qtpylib.crossed_above(dataframe["rsi"], self.short_rsi.value))
|
||||
& (dataframe["tema"] > dataframe["bb_middleband"]) # Guard: tema above BB middle
|
||||
& (dataframe["tema"] < dataframe["tema"].shift(1)) # Guard: tema is falling
|
||||
& (dataframe["volume"] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
"enter_short",
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the exit signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with exit columns populated
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
# Signal: RSI crosses above 70
|
||||
(qtpylib.crossed_above(dataframe["rsi"], self.sell_rsi.value))
|
||||
& (dataframe["tema"] > dataframe["bb_middleband"]) # Guard: tema above BB middle
|
||||
& (dataframe["tema"] < dataframe["tema"].shift(1)) # Guard: tema is falling
|
||||
& (dataframe["volume"] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
"exit_long",
|
||||
] = 1
|
||||
|
||||
dataframe.loc[
|
||||
(
|
||||
# Signal: RSI crosses above 30
|
||||
(qtpylib.crossed_above(dataframe["rsi"], self.exit_short_rsi.value))
|
||||
&
|
||||
# Guard: tema below BB middle
|
||||
(dataframe["tema"] <= dataframe["bb_middleband"])
|
||||
& (dataframe["tema"] > dataframe["tema"].shift(1)) # Guard: tema is raising
|
||||
& (dataframe["volume"] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
"exit_short",
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
@ -1,480 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Strategy analysis example\n",
|
||||
"\n",
|
||||
"Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n",
|
||||
"The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location.\n",
|
||||
"Please follow the [documentation](https://www.freqtrade.io/en/stable/data-download/) for more details."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"### Change Working directory to repository root"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Change directory\n",
|
||||
"# Modify this cell to insure that the output shows the correct path.\n",
|
||||
"# Define all paths relative to the project root shown in the cell output\n",
|
||||
"project_root = \"somedir/freqtrade\"\n",
|
||||
"i = 0\n",
|
||||
"try:\n",
|
||||
" os.chdir(project_root)\n",
|
||||
" if not Path(\"LICENSE\").is_file():\n",
|
||||
" i = 0\n",
|
||||
" while i < 4 and (not Path(\"LICENSE\").is_file()):\n",
|
||||
" os.chdir(Path(Path.cwd(), \"../\"))\n",
|
||||
" i += 1\n",
|
||||
" project_root = Path.cwd()\n",
|
||||
"except FileNotFoundError:\n",
|
||||
" print(\"Please define the project root relative to the current directory\")\n",
|
||||
"print(Path.cwd())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Configure Freqtrade environment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from freqtrade.configuration import Configuration\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Customize these according to your needs.\n",
|
||||
"\n",
|
||||
"# Initialize empty configuration object\n",
|
||||
"config = Configuration.from_files([])\n",
|
||||
"# Optionally (recommended), use existing configuration file\n",
|
||||
"# config = Configuration.from_files([\"user_data/config.json\"])\n",
|
||||
"\n",
|
||||
"# Define some constants\n",
|
||||
"config[\"timeframe\"] = \"5m\"\n",
|
||||
"# Name of the strategy class\n",
|
||||
"config[\"strategy\"] = \"SampleStrategy\"\n",
|
||||
"# Location of the data\n",
|
||||
"data_location = config[\"datadir\"]\n",
|
||||
"# Pair to analyze - Only use one pair here\n",
|
||||
"pair = \"BTC/USDT\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Load data using values set above\n",
|
||||
"from freqtrade.data.history import load_pair_history\n",
|
||||
"from freqtrade.enums import CandleType\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"candles = load_pair_history(\n",
|
||||
" datadir=data_location,\n",
|
||||
" timeframe=config[\"timeframe\"],\n",
|
||||
" pair=pair,\n",
|
||||
" data_format=\"json\", # Make sure to update this to your data\n",
|
||||
" candle_type=CandleType.SPOT,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Confirm success\n",
|
||||
"print(f\"Loaded {len(candles)} rows of data for {pair} from {data_location}\")\n",
|
||||
"candles.head()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Load and run strategy\n",
|
||||
"* Rerun each time the strategy file is changed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Load strategy using values set above\n",
|
||||
"from freqtrade.data.dataprovider import DataProvider\n",
|
||||
"from freqtrade.resolvers import StrategyResolver\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"strategy = StrategyResolver.load_strategy(config)\n",
|
||||
"strategy.dp = DataProvider(config, None, None)\n",
|
||||
"strategy.ft_bot_start()\n",
|
||||
"\n",
|
||||
"# Generate buy/sell signals using strategy\n",
|
||||
"df = strategy.analyze_ticker(candles, {\"pair\": pair})\n",
|
||||
"df.tail()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Display the trade details\n",
|
||||
"\n",
|
||||
"* Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n",
|
||||
"* Some possible problems\n",
|
||||
" * Columns with NaN values at the end of the dataframe\n",
|
||||
" * Columns used in `crossed*()` functions with completely different units\n",
|
||||
"* Comparison with full backtest\n",
|
||||
" * having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n",
|
||||
" * Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available. \n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Report results\n",
|
||||
"print(f\"Generated {df['enter_long'].sum()} entry signals\")\n",
|
||||
"data = df.set_index(\"date\", drop=False)\n",
|
||||
"data.tail()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Load existing objects into a Jupyter notebook\n",
|
||||
"\n",
|
||||
"The following cells assume that you have already generated data using the cli. \n",
|
||||
"They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Load backtest results to pandas dataframe\n",
|
||||
"\n",
|
||||
"Analyze a trades dataframe (also used below for plotting)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n",
|
||||
"backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
|
||||
"# backtest_dir can also point to a specific file\n",
|
||||
"# backtest_dir = (\n",
|
||||
"# config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"\n",
|
||||
"# )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# You can get the full backtest statistics by using the following command.\n",
|
||||
"# This contains all information used to generate the backtest result.\n",
|
||||
"stats = load_backtest_stats(backtest_dir)\n",
|
||||
"\n",
|
||||
"strategy = \"SampleStrategy\"\n",
|
||||
"# All statistics are available per strategy, so if `--strategy-list` was used during backtest,\n",
|
||||
"# this will be reflected here as well.\n",
|
||||
"# Example usages:\n",
|
||||
"print(stats[\"strategy\"][strategy][\"results_per_pair\"])\n",
|
||||
"# Get pairlist used for this backtest\n",
|
||||
"print(stats[\"strategy\"][strategy][\"pairlist\"])\n",
|
||||
"# Get market change (average change of all pairs from start to end of the backtest period)\n",
|
||||
"print(stats[\"strategy\"][strategy][\"market_change\"])\n",
|
||||
"# Maximum drawdown ()\n",
|
||||
"print(stats[\"strategy\"][strategy][\"max_drawdown_abs\"])\n",
|
||||
"# Maximum drawdown start and end\n",
|
||||
"print(stats[\"strategy\"][strategy][\"drawdown_start\"])\n",
|
||||
"print(stats[\"strategy\"][strategy][\"drawdown_end\"])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Get strategy comparison (only relevant if multiple strategies were compared)\n",
|
||||
"print(stats[\"strategy_comparison\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Load backtested trades as dataframe\n",
|
||||
"trades = load_backtest_data(backtest_dir)\n",
|
||||
"\n",
|
||||
"# Show value-counts per pair\n",
|
||||
"trades.groupby(\"pair\")[\"exit_reason\"].value_counts()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Plotting daily profit / equity line"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n",
|
||||
"\n",
|
||||
"import pandas as pd\n",
|
||||
"import plotly.express as px\n",
|
||||
"\n",
|
||||
"from freqtrade.configuration import Configuration\n",
|
||||
"from freqtrade.data.btanalysis import load_backtest_stats\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# strategy = 'SampleStrategy'\n",
|
||||
"# config = Configuration.from_files([\"user_data/config.json\"])\n",
|
||||
"# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n",
|
||||
"\n",
|
||||
"stats = load_backtest_stats(backtest_dir)\n",
|
||||
"strategy_stats = stats[\"strategy\"][strategy]\n",
|
||||
"\n",
|
||||
"df = pd.DataFrame(columns=[\"dates\", \"equity\"], data=strategy_stats[\"daily_profit\"])\n",
|
||||
"df[\"equity_daily\"] = df[\"equity\"].cumsum()\n",
|
||||
"\n",
|
||||
"fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n",
|
||||
"fig.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Load live trading results into a pandas dataframe\n",
|
||||
"\n",
|
||||
"In case you did already some trading and want to analyze your performance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from freqtrade.data.btanalysis import load_trades_from_db\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Fetch trades from database\n",
|
||||
"trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n",
|
||||
"\n",
|
||||
"# Display results\n",
|
||||
"trades.groupby(\"pair\")[\"exit_reason\"].value_counts()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Analyze the loaded trades for trade parallelism\n",
|
||||
"This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with a very high `max_open_trades` setting.\n",
|
||||
"\n",
|
||||
"`analyze_trade_parallelism()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from freqtrade.data.btanalysis import analyze_trade_parallelism\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Analyze the above\n",
|
||||
"parallel_trades = analyze_trade_parallelism(trades, \"5m\")\n",
|
||||
"\n",
|
||||
"parallel_trades.plot()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Plot results\n",
|
||||
"\n",
|
||||
"Freqtrade offers interactive plotting capabilities based on plotly."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from freqtrade.plot.plotting import generate_candlestick_graph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Limit graph period to keep plotly quick and reactive\n",
|
||||
"\n",
|
||||
"# Filter trades to one pair\n",
|
||||
"trades_red = trades.loc[trades[\"pair\"] == pair]\n",
|
||||
"\n",
|
||||
"data_red = data[\"2019-06-01\":\"2019-06-10\"]\n",
|
||||
"# Generate candlestick graph\n",
|
||||
"graph = generate_candlestick_graph(\n",
|
||||
" pair=pair,\n",
|
||||
" data=data_red,\n",
|
||||
" trades=trades_red,\n",
|
||||
" indicators1=[\"sma20\", \"ema50\", \"ema55\"],\n",
|
||||
" indicators2=[\"rsi\", \"macd\", \"macdsignal\", \"macdhist\"],\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Show graph inline\n",
|
||||
"# graph.show()\n",
|
||||
"\n",
|
||||
"# Render graph in a separate window\n",
|
||||
"graph.show(renderer=\"browser\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Plot average profit per trade as distribution graph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import plotly.figure_factory as ff\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"hist_data = [trades.profit_ratio]\n",
|
||||
"group_labels = [\"profit_ratio\"] # name of the dataset\n",
|
||||
"\n",
|
||||
"fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01)\n",
|
||||
"fig.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"file_extension": ".py",
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3.9.7 64-bit",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
},
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"npconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"toc": {
|
||||
"base_numbering": 1,
|
||||
"nav_menu": {},
|
||||
"number_sections": true,
|
||||
"sideBar": true,
|
||||
"skip_h1_title": false,
|
||||
"title_cell": "Table of Contents",
|
||||
"title_sidebar": "Contents",
|
||||
"toc_cell": false,
|
||||
"toc_position": {},
|
||||
"toc_section_display": true,
|
||||
"toc_window_display": false
|
||||
},
|
||||
"varInspector": {
|
||||
"cols": {
|
||||
"lenName": 16,
|
||||
"lenType": 16,
|
||||
"lenVar": 40
|
||||
},
|
||||
"kernels_config": {
|
||||
"python": {
|
||||
"delete_cmd_postfix": "",
|
||||
"delete_cmd_prefix": "del ",
|
||||
"library": "var_list.py",
|
||||
"varRefreshCmd": "print(var_dic_list())"
|
||||
},
|
||||
"r": {
|
||||
"delete_cmd_postfix": ") ",
|
||||
"delete_cmd_prefix": "rm(",
|
||||
"library": "var_list.r",
|
||||
"varRefreshCmd": "cat(var_dic_list()) "
|
||||
}
|
||||
},
|
||||
"types_to_exclude": [
|
||||
"module",
|
||||
"function",
|
||||
"builtin_function_or_method",
|
||||
"instance",
|
||||
"_Feature"
|
||||
],
|
||||
"window_display": false
|
||||
},
|
||||
"version": 3,
|
||||
"vscode": {
|
||||
"interpreter": {
|
||||
"hash": "675f32a300d6d26767470181ad0b11dd4676bcce7ed1dd2ffe2fbc370c95fc7c"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@ -1,230 +0,0 @@
|
||||
import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
|
||||
|
||||
class TheForceV7V2(IStrategy):
|
||||
|
||||
INTERFACE_VERSION = 2
|
||||
|
||||
minimal_roi = {
|
||||
"0": 10
|
||||
}
|
||||
stoploss = -0.1
|
||||
trailing_stop = False
|
||||
timeframe = '5m'
|
||||
process_only_new_candles = False
|
||||
use_sell_signal = True
|
||||
sell_profit_only = False
|
||||
ignore_roi_if_buy_signal = True
|
||||
startup_candle_count: int = 30
|
||||
order_types = {
|
||||
'buy': 'limit',
|
||||
'sell': 'limit',
|
||||
'stoploss': 'market',
|
||||
'stoploss_on_exchange': False
|
||||
}
|
||||
order_time_in_force = {
|
||||
'buy': 'gtc',
|
||||
'sell': 'gtc'
|
||||
}
|
||||
plot_config = {
|
||||
|
||||
'main_plot': {
|
||||
'tema': {},
|
||||
'sar': {'color': 'white'},
|
||||
},
|
||||
'subplots': {
|
||||
"MACD": {
|
||||
'macd': {'color': 'blue'},
|
||||
'macdsignal': {'color': 'orange'},
|
||||
},
|
||||
"RSI": {
|
||||
'rsi': {'color': 'red'},
|
||||
}
|
||||
}
|
||||
}
|
||||
def informative_pairs(self):
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
These pair/interval combinations are non-tradeable, unless they are part
|
||||
of the whitelist as well.
|
||||
For more information, please consult the documentation
|
||||
:return: List of tuples in the format (pair, interval)
|
||||
Sample: return [("ETH/USDT", "5m"),
|
||||
("BTC/USDT", "15m"),
|
||||
]
|
||||
"""
|
||||
return []
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
||||
|
||||
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
|
||||
dataframe['rsi7'] = ta.RSI(dataframe, timeperiod=7)
|
||||
|
||||
macd = ta.MACD(dataframe,12,26,1)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
dataframe['ema5h'] = ta.EMA(dataframe['high'], timeperiod=5)
|
||||
dataframe['ema5l'] = ta.EMA(dataframe['low'], timeperiod=5)
|
||||
dataframe['ema5c'] = ta.EMA(dataframe['close'], timeperiod=5)
|
||||
dataframe['ema5o'] = ta.EMA(dataframe['open'], timeperiod=5)
|
||||
dataframe['ema200c'] = ta.MA(dataframe['close'], 200)
|
||||
|
||||
dataframe['volvar'] = (dataframe['volume'].rolling(100).mean() * 1.5)
|
||||
|
||||
bollinger = qtpylib.bollinger_bands(dataframe['close'], window=21, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
(
|
||||
( #Original buy condition
|
||||
(dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
|
||||
&
|
||||
(dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
|
||||
)
|
||||
|
|
||||
( #V3 added based on SmoothScalp
|
||||
(dataframe['slowk'] < 30) & (dataframe['slowd'] < 30) &
|
||||
(qtpylib.crossed_above(dataframe['slowk'], dataframe['slowd']))
|
||||
)
|
||||
)
|
||||
&
|
||||
( #Original buy condition #Might need improvement to have better signals
|
||||
(dataframe['macd'] > dataframe['macd'].shift(1))
|
||||
&
|
||||
(dataframe['macdsignal'] > dataframe['macdsignal'].shift(1))
|
||||
)
|
||||
&
|
||||
( #Original buy condition
|
||||
(dataframe['close'] > dataframe['close'].shift(1))
|
||||
& #V6 added condition to improve buy's
|
||||
(dataframe['open'] > dataframe['open'].shift(1))
|
||||
)
|
||||
&
|
||||
( #Original buy condition
|
||||
(dataframe['ema5c'] >= dataframe['ema5o'])
|
||||
|
|
||||
(dataframe['open'] < dataframe['ema5l'])
|
||||
)
|
||||
&
|
||||
(
|
||||
|
||||
(dataframe['volume'] > dataframe['volvar'])
|
||||
)
|
||||
)
|
||||
|
|
||||
( # V2 Added buy condition w/ Bollingers bands
|
||||
(dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
|
||||
&
|
||||
(dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
|
||||
&
|
||||
(
|
||||
(dataframe['close'] <= dataframe['bb_lowerband'])
|
||||
|
|
||||
(dataframe['open'] <= dataframe['bb_lowerband'])
|
||||
)
|
||||
)
|
||||
|
|
||||
( # V5 added Pullback RSI thanks to simoelmou
|
||||
(dataframe['close'] > dataframe['ema200c'])
|
||||
&
|
||||
(dataframe['rsi7'] < 35)
|
||||
)
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
(
|
||||
( #Original sell condition
|
||||
(dataframe['slowk'] <= 80) & (dataframe['slowd'] <= 80)
|
||||
)
|
||||
|
|
||||
( #V3 added based on SmoothScalp
|
||||
(qtpylib.crossed_above(dataframe['slowk'], 70))
|
||||
|
|
||||
(qtpylib.crossed_above(dataframe['slowd'], 70))
|
||||
)
|
||||
)
|
||||
&
|
||||
( #Original sell condition
|
||||
(dataframe['macd'] < dataframe['macd'].shift(1))
|
||||
&
|
||||
(dataframe['macdsignal'] < dataframe['macdsignal'].shift(1))
|
||||
)
|
||||
&
|
||||
( #Original sell condition
|
||||
(dataframe['ema5c'] < dataframe['ema5o'])
|
||||
|
|
||||
(dataframe['open'] >= dataframe['ema5h']) # V3 added based on SmoothScalp
|
||||
)
|
||||
)
|
||||
|
|
||||
( # V2 Added sell condition w/ Bollingers bands
|
||||
(dataframe['slowk'] <= 80)
|
||||
&
|
||||
(dataframe['slowd'] <= 80)
|
||||
&
|
||||
(
|
||||
(dataframe['close'] >= dataframe['bb_upperband'])
|
||||
|
|
||||
(dataframe['open'] >= dataframe['bb_upperband'])
|
||||
)
|
||||
)
|
||||
|
|
||||
(# V6 Added sell condition for extra high values
|
||||
(dataframe['high'] > dataframe['bb_upperband'])
|
||||
&
|
||||
(((dataframe['high'] - dataframe['bb_upperband']) * 100 / dataframe['bb_upperband']) > 1)
|
||||
)
|
||||
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
@ -1,206 +0,0 @@
|
||||
1,4d0
|
||||
< import numpy as np # noqa
|
||||
< import pandas as pd # noqa
|
||||
< from pandas import DataFrame
|
||||
<
|
||||
6c2,3
|
||||
<
|
||||
---
|
||||
> import pandas as pd
|
||||
> import numpy as np
|
||||
12,13c9
|
||||
<
|
||||
< INTERFACE_VERSION = 2
|
||||
---
|
||||
> INTERFACE_VERSION = 3
|
||||
14a11
|
||||
> # 基础参数
|
||||
16c13
|
||||
< "0": 10
|
||||
---
|
||||
> "0": 10
|
||||
36d32
|
||||
< plot_config = {
|
||||
37a34,35
|
||||
> # 绘图配置
|
||||
> plot_config = {
|
||||
51a50
|
||||
>
|
||||
65c64
|
||||
< def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
---
|
||||
> def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
77,78d75
|
||||
<
|
||||
<
|
||||
85c82
|
||||
< macd = ta.MACD(dataframe,12,26,1)
|
||||
---
|
||||
> macd = ta.MACD(dataframe, 12, 26, 1)
|
||||
102,103c99
|
||||
<
|
||||
<
|
||||
---
|
||||
>
|
||||
106c102
|
||||
< def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
---
|
||||
> def populate_buy_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
116,130c112,113
|
||||
< (
|
||||
< ( #Original buy condition
|
||||
< (dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
|
||||
< &
|
||||
< (dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
|
||||
< )
|
||||
< |
|
||||
< ( #V3 added based on SmoothScalp
|
||||
< (dataframe['slowk'] < 30) & (dataframe['slowd'] < 30) &
|
||||
< (qtpylib.crossed_above(dataframe['slowk'], dataframe['slowd']))
|
||||
< )
|
||||
< )
|
||||
< &
|
||||
< ( #Original buy condition #Might need improvement to have better signals
|
||||
< (dataframe['macd'] > dataframe['macd'].shift(1))
|
||||
---
|
||||
> (
|
||||
> (dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
|
||||
132c115
|
||||
< (dataframe['macdsignal'] > dataframe['macdsignal'].shift(1))
|
||||
---
|
||||
> (dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
|
||||
134,149c117,120
|
||||
< &
|
||||
< ( #Original buy condition
|
||||
< (dataframe['close'] > dataframe['close'].shift(1))
|
||||
< & #V6 added condition to improve buy's
|
||||
< (dataframe['open'] > dataframe['open'].shift(1))
|
||||
< )
|
||||
< &
|
||||
< ( #Original buy condition
|
||||
< (dataframe['ema5c'] >= dataframe['ema5o'])
|
||||
< |
|
||||
< (dataframe['open'] < dataframe['ema5l'])
|
||||
< )
|
||||
< &
|
||||
< (
|
||||
<
|
||||
< (dataframe['volume'] > dataframe['volvar'])
|
||||
---
|
||||
> |
|
||||
> (
|
||||
> (dataframe['slowk'] < 30) & (dataframe['slowd'] < 30) &
|
||||
> (qtpylib.crossed_above(dataframe['slowk'], dataframe['slowd']))
|
||||
152,154c123,125
|
||||
< |
|
||||
< ( # V2 Added buy condition w/ Bollingers bands
|
||||
< (dataframe['slowk'] >= 20) & (dataframe['slowk'] <= 80)
|
||||
---
|
||||
> &
|
||||
> (
|
||||
> (dataframe['macd'] > dataframe['macd'].shift(1))
|
||||
156c127,131
|
||||
< (dataframe['slowd'] >= 20) & (dataframe['slowd'] <= 80)
|
||||
---
|
||||
> (dataframe['macdsignal'] > dataframe['macdsignal'].shift(1))
|
||||
> )
|
||||
> &
|
||||
> (
|
||||
> (dataframe['close'] > dataframe['close'].shift(1))
|
||||
158,162c133
|
||||
< (
|
||||
< (dataframe['close'] <= dataframe['bb_lowerband'])
|
||||
< |
|
||||
< (dataframe['open'] <= dataframe['bb_lowerband'])
|
||||
< )
|
||||
---
|
||||
> (dataframe['open'] > dataframe['open'].shift(1))
|
||||
164,168c135,143
|
||||
< |
|
||||
< ( # V5 added Pullback RSI thanks to simoelmou
|
||||
< (dataframe['close'] > dataframe['ema200c'])
|
||||
< &
|
||||
< (dataframe['rsi7'] < 35)
|
||||
---
|
||||
> &
|
||||
> (
|
||||
> (dataframe['ema5c'] >= dataframe['ema5o'])
|
||||
> |
|
||||
> (dataframe['open'] < dataframe['ema5l'])
|
||||
> )
|
||||
> &
|
||||
> (
|
||||
> (dataframe['volume'] > dataframe['volvar'])
|
||||
175c150
|
||||
< def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
---
|
||||
> def populate_sell_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
|
||||
185,200c160,161
|
||||
< (
|
||||
< ( #Original sell condition
|
||||
< (dataframe['slowk'] <= 80) & (dataframe['slowd'] <= 80)
|
||||
< )
|
||||
< |
|
||||
< ( #V3 added based on SmoothScalp
|
||||
< (qtpylib.crossed_above(dataframe['slowk'], 70))
|
||||
< |
|
||||
< (qtpylib.crossed_above(dataframe['slowd'], 70))
|
||||
< )
|
||||
< )
|
||||
< &
|
||||
< ( #Original sell condition
|
||||
< (dataframe['macd'] < dataframe['macd'].shift(1))
|
||||
< &
|
||||
< (dataframe['macdsignal'] < dataframe['macdsignal'].shift(1))
|
||||
---
|
||||
> (
|
||||
> (dataframe['slowk'] <= 80) & (dataframe['slowd'] <= 80)
|
||||
202,204c163,165
|
||||
< &
|
||||
< ( #Original sell condition
|
||||
< (dataframe['ema5c'] < dataframe['ema5o'])
|
||||
---
|
||||
> |
|
||||
> (
|
||||
> (qtpylib.crossed_above(dataframe['slowk'], 70))
|
||||
206c167
|
||||
< (dataframe['open'] >= dataframe['ema5h']) # V3 added based on SmoothScalp
|
||||
---
|
||||
> (qtpylib.crossed_above(dataframe['slowd'], 70))
|
||||
209,213c170,172
|
||||
< |
|
||||
< ( # V2 Added sell condition w/ Bollingers bands
|
||||
< (dataframe['slowk'] <= 80)
|
||||
< &
|
||||
< (dataframe['slowd'] <= 80)
|
||||
---
|
||||
> &
|
||||
> (
|
||||
> (dataframe['macd'] < dataframe['macd'].shift(1))
|
||||
215,219c174
|
||||
< (
|
||||
< (dataframe['close'] >= dataframe['bb_upperband'])
|
||||
< |
|
||||
< (dataframe['open'] >= dataframe['bb_upperband'])
|
||||
< )
|
||||
---
|
||||
> (dataframe['macdsignal'] < dataframe['macdsignal'].shift(1))
|
||||
221,225c176,180
|
||||
< |
|
||||
< (# V6 Added sell condition for extra high values
|
||||
< (dataframe['high'] > dataframe['bb_upperband'])
|
||||
< &
|
||||
< (((dataframe['high'] - dataframe['bb_upperband']) * 100 / dataframe['bb_upperband']) > 1)
|
||||
---
|
||||
> &
|
||||
> (
|
||||
> (dataframe['ema5c'] < dataframe['ema5o'])
|
||||
> |
|
||||
> (dataframe['open'] >= dataframe['ema5h'])
|
||||
227d181
|
||||
<
|
||||
230,231c184
|
||||
< return dataframe
|
||||
<
|
||||
---
|
||||
>
|
||||
74
tools/analytic.py
Normal file
74
tools/analytic.py
Normal file
@ -0,0 +1,74 @@
|
||||
import pandas as pd
|
||||
|
||||
# 加载交易记录
|
||||
df = pd.read_csv('../result/backtest_trades.csv')
|
||||
|
||||
# 转换日期格式
|
||||
df['open_date'] = pd.to_datetime(df['open_date'])
|
||||
df['close_date'] = pd.to_datetime(df['close_date'])
|
||||
|
||||
# 计算持仓天数
|
||||
df['holding_days'] = (df['close_date'] - df['open_date']).dt.total_seconds() / (60 * 60 * 24)
|
||||
|
||||
# 按币种分组
|
||||
grouped_by_pair = df.groupby('pair').agg(
|
||||
total_trades=('profit_abs', 'size'),
|
||||
avg_profit_ratio=('profit_ratio', 'mean'),
|
||||
total_profit_abs=('profit_abs', 'sum'),
|
||||
win_rate=('profit_ratio', lambda x: (x > 0).mean()),
|
||||
avg_duration=('trade_duration', 'mean')
|
||||
)
|
||||
|
||||
print(grouped_by_pair)
|
||||
|
||||
#按退出原因分组
|
||||
grouped_by_exit = df.groupby('exit_reason').agg(
|
||||
total_trades=('profit_abs', 'size'),
|
||||
avg_profit_ratio=('profit_ratio', 'mean'),
|
||||
total_profit_abs=('profit_abs', 'sum'),
|
||||
win_rate=('profit_ratio', lambda x: (x > 0).mean())
|
||||
)
|
||||
|
||||
print(grouped_by_exit)
|
||||
#按月份分组统计
|
||||
df['open_date_naive'] = df['open_date'].dt.tz_localize(None)
|
||||
df['month'] = df['open_date_naive'].dt.to_period('M')
|
||||
|
||||
|
||||
grouped_by_month = df.groupby('month').agg(
|
||||
total_trades=('profit_abs', 'size'),
|
||||
avg_profit_ratio=('profit_ratio', 'mean'),
|
||||
total_profit_abs=('profit_abs', 'sum'),
|
||||
win_rate=('profit_ratio', lambda x: (x > 0).mean())
|
||||
)
|
||||
|
||||
print(grouped_by_month)
|
||||
# 按盈利区间分组统计
|
||||
bins = [-float('inf'), -0.05, -0.02, 0, 0.02, 0.05, float('inf')]
|
||||
labels = ['<-5%', '-5%~-2%', '-2%~0%', '0%~2%', '2%~5%', '>5%']
|
||||
|
||||
df['profit_group'] = pd.cut(df['profit_ratio'], bins=bins, labels=labels)
|
||||
|
||||
grouped_by_profit = df.groupby('profit_group', observed=True).agg(
|
||||
count=('profit_abs', 'size'),
|
||||
avg_profit=('profit_ratio', 'mean'),
|
||||
total_profit=('profit_abs', 'sum')
|
||||
)
|
||||
|
||||
print(grouped_by_profit)
|
||||
# 分组为短中长线
|
||||
df['duration_group'] = pd.cut(df['trade_duration'],
|
||||
bins=[0, 60, 360, 1440, 100000],
|
||||
labels=['<1小时', '1~6小时', '6小时~1天', '>1天'])
|
||||
|
||||
grouped_by_duration = df.groupby('duration_group', observed=True).agg(
|
||||
count=('profit_abs', 'size'),
|
||||
avg_profit=('profit_ratio', 'mean'),
|
||||
win_rate=('profit_ratio', lambda x: (x > 0).mean()),
|
||||
total_profit=('profit_abs', 'sum')
|
||||
)
|
||||
|
||||
print(grouped_by_duration)
|
||||
|
||||
|
||||
|
||||
4
tools/analytic.sh
Executable file
4
tools/analytic.sh
Executable file
@ -0,0 +1,4 @@
|
||||
cd ../
|
||||
source .venv/bin/activate
|
||||
cd -
|
||||
python analytic.py
|
||||
@ -31,15 +31,18 @@ rm -rf user_data/models/*
|
||||
rm -rf ./freqtrade/user_data/data/backtest_results/*
|
||||
rm -fr ./user_data/dryrun_results/*
|
||||
|
||||
hyperopt_config="${STRATEGY_NAME%.py}.json"
|
||||
#docker-compose -f docker-compose_backtest.yml run --rm freqtrade >output.log 2>&1
|
||||
freqtrade backtesting \
|
||||
--logfile ./user_data/logs/freqtrade.log \
|
||||
--freqaimodel XGBoostRegressor \
|
||||
--strategy $STRATEGY_NAME \
|
||||
--config ./freqtrade/templates/$hyperopt_config \
|
||||
--config config_examples/$CONFIG_FILE \
|
||||
--strategy-path ./freqtrade/templates \
|
||||
--timerange ${START_DATE}-${END_DATE} \
|
||||
--breakdown week month \
|
||||
--enable-protections \
|
||||
--export trades \
|
||||
--fee 0.0016 \
|
||||
--cache none >output.log 2>&1
|
||||
@ -71,4 +74,5 @@ sed -i 's/\x1B\[[0-9;]*m//g' output.log
|
||||
cp output.log result/ -f
|
||||
cd tools/
|
||||
python tradestocsv.py
|
||||
python analytic.py >../result/analytic.log
|
||||
cd ../
|
||||
|
||||
76
tools/hyperopt.sh
Executable file
76
tools/hyperopt.sh
Executable file
@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 检查 .env 文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "⚠️ 本地缺少 .env 文件,请创建并配置。示例内容如下:"
|
||||
echo ""
|
||||
echo "STRATEGY_NAME=TheForceV7"
|
||||
echo "CONFIG_FILE=basic.json"
|
||||
echo "TEST_BRANCH=theforce-noai-test"
|
||||
echo "DRYRUN_BRANCH=theforce-noai-dryrun"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 加载 .env 文件中的变量
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
# 设置默认值
|
||||
STRATEGY_NAME=${STRATEGY_NAME:-TheForceV7}
|
||||
CONFIG_FILE=${CONFIG_FILE:-basic.json}
|
||||
|
||||
echo "Using strategy: $STRATEGY_NAME"
|
||||
echo "Using config: $CONFIG_FILE"
|
||||
echo "Using testBranch: $TEST_BRANCH"
|
||||
# Parse command line arguments
|
||||
START_DATE=${1:-$(date -d "2 days ago" +"%Y%m%d")}
|
||||
END_DATE=${2:-$(date -d "tomorrow" +"%Y%m%d")}
|
||||
|
||||
cd ../
|
||||
source .venv/bin/activate
|
||||
rm -rf user_data/models/*
|
||||
rm -rf ./freqtrade/user_data/data/backtest_results/*
|
||||
rm -fr ./user_data/dryrun_results/*
|
||||
|
||||
#docker-compose -f docker-compose_backtest.yml run --rm freqtrade >output.log 2>&1
|
||||
freqtrade hyperopt \
|
||||
--logfile ./user_data/logs/freqtrade.log \
|
||||
--freqaimodel XGBoostRegressor \
|
||||
--strategy $STRATEGY_NAME \
|
||||
--config config_examples/$CONFIG_FILE \
|
||||
--strategy-path ./freqtrade/templates \
|
||||
--timerange ${START_DATE}-${END_DATE} \
|
||||
--epochs 100 \
|
||||
--hyperopt-loss ShortTradeDurHyperOptLoss \
|
||||
--spaces stoploss \
|
||||
--fee 0.0016
|
||||
#>output.log 2>&1
|
||||
#sed -i 's/\x1B\[[0-9;]*m//g' output.log
|
||||
|
||||
#python3 tools/filter.py
|
||||
|
||||
rm ./result/*.json -fr
|
||||
rm ./result/*.py -fr
|
||||
mv ./user_data/backtest_results/* ./result/
|
||||
|
||||
cd ./result
|
||||
# 查找当前目录下的所有 zip 文件
|
||||
zip_files=(*.zip)
|
||||
|
||||
# 检查是否只有一个 zip 文件
|
||||
if [ ${#zip_files[@]} -eq 1 ]; then
|
||||
# 解压缩该 zip 文件到当前目录
|
||||
unzip "${zip_files[0]}"
|
||||
rm *.zip
|
||||
rm *.feather
|
||||
else
|
||||
echo "当前目录下没有 zip 文件或者有多个 zip 文件,无法操作。"
|
||||
fi
|
||||
|
||||
cd -
|
||||
sed -i 's/\x1B\[[0-9;]*m//g' output.log
|
||||
#python3 ../filter.py
|
||||
cp output.log result/ -f
|
||||
cd tools/
|
||||
python tradestocsv.py
|
||||
python analytic.py >../result/analytic.log
|
||||
cd ../
|
||||
Loading…
x
Reference in New Issue
Block a user