解决了一个ccxt的bug, okx接口返回内容有baseCcy或者quoteCcy为null, 挂载了一个新的文件替换了容器内的版本, 算是临时解决了,以后要考虑如何持续集成的问题

This commit is contained in:
zhangkun9038@dingtalk.com 2025-04-25 15:23:24 +08:00
parent f815ee89ee
commit 05122af764
4 changed files with 8380 additions and 69 deletions

8325
ccxt/async_support/okx.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -31,14 +31,17 @@
}, },
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": true, "enableRateLimit": true,
"rateLimit": 1000, "rateLimit": 500,
"timeout": 20000 "timeout": 20000
}, },
"pair_whitelist": [ "pair_whitelist": [
"BTC/USDT", "BTC/USDT",
"SOL/USDT" "SOL/USDT"
], ],
"pair_blacklist": [] "pair_blacklist": [
"LAYER/USD",
"LAYER/USDT"
]
}, },
"entry_pricing": { "entry_pricing": {
"price_side": "same", "price_side": "same",
@ -67,7 +70,7 @@
}, },
"freqaimodel": "CatboostClassifier", "freqaimodel": "CatboostClassifier",
"purge_old_models": 2, "purge_old_models": 2,
"identifier": "test130", "identifier": "test131",
"train_period_days": 30, "train_period_days": 30,
"backtest_period_days": 10, "backtest_period_days": 10,
"live_retrain_hours": 0, "live_retrain_hours": 0,

View File

@ -23,6 +23,7 @@ services:
- "./config_examples:/freqtrade/config_examples" - "./config_examples:/freqtrade/config_examples"
- "./freqtrade/templates:/freqtrade/templates" - "./freqtrade/templates:/freqtrade/templates"
- "./freqtrade/exchange/:/freqtrade/exchange" - "./freqtrade/exchange/:/freqtrade/exchange"
- "./ccxt/async_support/okx.py:/home/ftuser/.local/lib/python3.12/site-packages/ccxt/async_support/okx.py"
# Expose api on port 8080 (localhost only) # Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
# for more information. # for more information.

View File

@ -11,9 +11,9 @@ logger = logging.getLogger(__name__)
class FreqaiExampleStrategy(IStrategy): class FreqaiExampleStrategy(IStrategy):
minimal_roi = { minimal_roi = {
"0": 0.076, "0": 0.02,
"7": 0.034, "7": 0.01,
"13": 0.007, "13": 0.005,
"60": 0 "60": 0
} }
@ -24,29 +24,25 @@ class FreqaiExampleStrategy(IStrategy):
startup_candle_count: int = 40 startup_candle_count: int = 40
can_short = False can_short = False
# Hyperopt 参数
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=True, load=True) buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=True, load=True) sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=True, load=True)
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, 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_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) 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) stoploss_param = DecimalParameter(low=-0.25, high=-0.05, default=-0.1, space="stoploss", optimize=True, load=True)
trailing_stop_positive_offset = DecimalParameter(low=0.01, high=0.5, default=0.02, space="trailing", optimize=True, load=True) trailing_stop_positive_offset = DecimalParameter(low=0.005, high=0.5, default=0.01, space="trailing", optimize=True, load=True)
protections = [ protections = []
{"method": "StoplossGuard", "stop_duration": 60, "lookback_period": 120},
{"method": "MaxDrawdown", "lookback_period": 120, "max_allowed_drawdown": 0.05}
]
freqai_info = { freqai_info = {
"model": "LightGBMRegressor", "model": "LightGBMRegressor",
"feature_parameters": { "feature_parameters": {
"include_timeframes": ["5m"], "include_timeframes": ["5m"],
"include_corr_pairlist": ["SOL/USDT", "BTC/USDT"], "include_corr_pairlist": ["SOL/USDT"],
"label_period_candles": 12, "label_period_candles": 12,
"include_shifted_candles": 0, "include_shifted_candles": 0,
"include_periods": [10, 20], "include_periods": [20],
"DI_threshold": 3.0 "DI_threshold": 5.0
}, },
"data_split_parameters": { "data_split_parameters": {
"test_size": 0.2, "test_size": 0.2,
@ -62,14 +58,20 @@ class FreqaiExampleStrategy(IStrategy):
} }
plot_config = { plot_config = {
"main_plot": {}, "main_plot": {
"close": {"color": "blue"},
"bb_lowerband": {"color": "purple"}
},
"subplots": { "subplots": {
"&-buy_rsi": {"&-buy_rsi": {"color": "green"}}, "&-buy_rsi": {"&-buy_rsi": {"color": "green"}},
"&-sell_rsi": {"&-sell_rsi": {"color": "red"}}, "&-sell_rsi": {"&-sell_rsi": {"color": "red"}},
"&-stoploss": {"&-stoploss": {"color": "purple"}}, "rsi": {"rsi": {"color": "black"}},
"&-roi_0": {"&-roi_0": {"color": "orange"}},
"do_predict": {"do_predict": {"color": "brown"}}, "do_predict": {"do_predict": {"color": "brown"}},
}, "trade_signals": {
"enter_long": {"color": "green", "type": "scatter"},
"exit_long": {"color": "red", "type": "scatter"}
}
}
} }
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
@ -130,12 +132,10 @@ class FreqaiExampleStrategy(IStrategy):
logger.info(f"DataFrame rows: {len(dataframe)}") logger.info(f"DataFrame rows: {len(dataframe)}")
logger.info(f"Columns before freqai.start: {list(dataframe.columns)}") logger.info(f"Columns before freqai.start: {list(dataframe.columns)}")
# 验证输入数据
if "close" not in dataframe.columns or dataframe["close"].isna().all(): if "close" not in dataframe.columns or dataframe["close"].isna().all():
logger.error(f"DataFrame missing 'close' column or all NaN for pair: {metadata['pair']}") logger.error(f"DataFrame missing 'close' column or all NaN for pair: {metadata['pair']}")
raise ValueError("DataFrame missing valid 'close' column") raise ValueError("DataFrame missing valid 'close' column")
# 生成 RSI
if len(dataframe) < 14: if len(dataframe) < 14:
logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute rsi") logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute rsi")
dataframe["rsi"] = 50 dataframe["rsi"] = 50
@ -143,7 +143,6 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
logger.info(f"rsi stats: {dataframe['rsi'].describe().to_string()}") logger.info(f"rsi stats: {dataframe['rsi'].describe().to_string()}")
# 生成 %-volatility
if len(dataframe) < 20 or dataframe["close"].isna().any(): if len(dataframe) < 20 or dataframe["close"].isna().any():
logger.warning(f"DataFrame too short ({len(dataframe)} rows) or contains NaN in close, cannot compute %-volatility") logger.warning(f"DataFrame too short ({len(dataframe)} rows) or contains NaN in close, cannot compute %-volatility")
dataframe["%-volatility"] = 0 dataframe["%-volatility"] = 0
@ -154,7 +153,6 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["%-volatility"] = (dataframe["%-volatility"] - dataframe["%-volatility"].mean()) / dataframe["%-volatility"].std() dataframe["%-volatility"] = (dataframe["%-volatility"] - dataframe["%-volatility"].mean()) / dataframe["%-volatility"].std()
logger.info(f"%-volatility stats: {dataframe['%-volatility'].describe().to_string()}") logger.info(f"%-volatility stats: {dataframe['%-volatility'].describe().to_string()}")
# 生成 TEMA
if len(dataframe) < 9: if len(dataframe) < 9:
logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute tema") logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute tema")
dataframe["tema"] = dataframe["close"] dataframe["tema"] = dataframe["close"]
@ -165,7 +163,6 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["tema"] = dataframe["tema"].fillna(dataframe["close"]) dataframe["tema"] = dataframe["tema"].fillna(dataframe["close"])
logger.info(f"tema stats: {dataframe['tema'].describe().to_string()}") logger.info(f"tema stats: {dataframe['tema'].describe().to_string()}")
# 生成 Bollinger Bands
if len(dataframe) < 20: if len(dataframe) < 20:
logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute bb_lowerband") logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute bb_lowerband")
dataframe["bb_lowerband"] = dataframe["close"] dataframe["bb_lowerband"] = dataframe["close"]
@ -177,21 +174,6 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["bb_lowerband"] = dataframe["bb_lowerband"].fillna(dataframe["close"]) dataframe["bb_lowerband"] = dataframe["bb_lowerband"].fillna(dataframe["close"])
logger.info(f"bb_lowerband stats: {dataframe['bb_lowerband'].describe().to_string()}") logger.info(f"bb_lowerband stats: {dataframe['bb_lowerband'].describe().to_string()}")
# 生成 up_or_down
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
if len(dataframe) < label_period + 1:
logger.warning(f"DataFrame too short ({len(dataframe)} rows), cannot compute up_or_down")
dataframe["up_or_down"] = 0
else:
dataframe["up_or_down"] = np.where(
dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0
)
if dataframe["up_or_down"].isna().any():
logger.warning("up_or_down contains NaN, filling with 0")
dataframe["up_or_down"] = dataframe["up_or_down"].fillna(0)
logger.info(f"up_or_down stats: {dataframe['up_or_down'].describe().to_string()}")
# 生成其他特征
if "date" in dataframe.columns: if "date" in dataframe.columns:
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
@ -200,7 +182,6 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["%-day_of_week"] = 0 dataframe["%-day_of_week"] = 0
dataframe["%-hour_of_day"] = 0 dataframe["%-hour_of_day"] = 0
# 调用 FreqAI
try: try:
dataframe = self.freqai.start(dataframe, metadata, self) dataframe = self.freqai.start(dataframe, metadata, self)
logger.info(f"Columns after freqai.start: {list(dataframe.columns)}") logger.info(f"Columns after freqai.start: {list(dataframe.columns)}")
@ -210,26 +191,23 @@ class FreqaiExampleStrategy(IStrategy):
dataframe["sell_rsi_pred"] = 80 dataframe["sell_rsi_pred"] = 80
dataframe["do_predict"] = 1 dataframe["do_predict"] = 1
# 检查预测列
for col in ["buy_rsi_pred", "sell_rsi_pred"]: for col in ["buy_rsi_pred", "sell_rsi_pred"]:
if col not in dataframe.columns: if col not in dataframe.columns:
logger.error(f"Error: {col} column not generated for pair: {metadata['pair']}") logger.error(f"Error: {col} column not generated for pair: {metadata['pair']}")
dataframe[col] = 50 if col == "buy_rsi_pred" else 80 dataframe[col] = 50 if col == "buy_rsi_pred" else 80
logger.info(f"{col} stats: {dataframe[col].describe().to_string()}") logger.info(f"{col} stats: {dataframe[col].describe().to_string()}")
# 调试特征分布 if "%-bb_width-period_20_SOL/USDT_5m" in dataframe.columns:
if "%-bb_width-period_10_SOL/USDT_5m" in dataframe.columns: if dataframe["%-bb_width-period_20_SOL/USDT_5m"].std() > 0:
if dataframe["%-bb_width-period_10_SOL/USDT_5m"].std() > 0: dataframe["%-bb_width-period_20_SOL/USDT_5m"] = (
dataframe["%-bb_width-period_10_SOL/USDT_5m"] = ( dataframe["%-bb_width-period_20_SOL/USDT_5m"] - dataframe["%-bb_width-period_20_SOL/USDT_5m"].mean()
dataframe["%-bb_width-period_10_SOL/USDT_5m"] - dataframe["%-bb_width-period_10_SOL/USDT_5m"].mean() ) / dataframe["%-bb_width-period_20_SOL/USDT_5m"].std()
) / dataframe["%-bb_width-period_10_SOL/USDT_5m"].std() logger.info(f"%-bb_width-period_20 stats: {dataframe['%-bb_width-period_20_SOL/USDT_5m'].describe().to_string()}")
logger.info(f"%-bb_width-period_10 stats: {dataframe['%-bb_width-period_10_SOL/USDT_5m'].describe().to_string()}")
# 动态生成期望的特征列
def get_expected_columns(freqai_config: dict) -> list: def get_expected_columns(freqai_config: dict) -> list:
indicators = ["rsi", "bb_width", "pct-change"] indicators = ["rsi", "bb_width", "pct-change"]
periods = freqai_config.get("feature_parameters", {}).get("include_periods", [10, 20]) periods = freqai_config.get("feature_parameters", {}).get("include_periods", [20])
pairs = freqai_config.get("include_corr_pairlist", ["SOL/USDT", "BTC/USDT"]) pairs = freqai_config.get("include_corr_pairlist", ["SOL/USDT"])
timeframes = freqai_config.get("include_timeframes", ["5m"]) timeframes = freqai_config.get("include_timeframes", ["5m"])
shifts = [0] shifts = [0]
expected_columns = ["%-volatility", "%-day_of_week", "%-hour_of_day"] expected_columns = ["%-volatility", "%-day_of_week", "%-hour_of_day"]
@ -248,50 +226,47 @@ class FreqaiExampleStrategy(IStrategy):
expected_columns = get_expected_columns(self.freqai_info) expected_columns = get_expected_columns(self.freqai_info)
logger.info(f"Expected feature columns ({len(expected_columns)}): {expected_columns[:10]}...") logger.info(f"Expected feature columns ({len(expected_columns)}): {expected_columns[:10]}...")
# 比较特征集
actual_columns = list(dataframe.columns) actual_columns = list(dataframe.columns)
missing_columns = [col for col in expected_columns if col not in actual_columns] missing_columns = [col for col in expected_columns if col not in actual_columns]
extra_columns = [col for col in actual_columns if col not in expected_columns and col.startswith("%-")] extra_columns = [col for col in actual_columns if col not in expected_columns and col.startswith("%-")]
logger.info(f"Missing columns ({len(missing_columns)}): {missing_columns}") logger.info(f"Missing columns ({len(missing_columns)}): {missing_columns}")
logger.info(f"Extra columns ({len(extra_columns)}): {extra_columns}") logger.info(f"Extra columns ({len(extra_columns)}): {extra_columns}")
# 调试 DI 丢弃预测
if "DI_values" in dataframe.columns: if "DI_values" in dataframe.columns:
logger.info(f"DI_values stats: {dataframe['DI_values'].describe().to_string()}") logger.info(f"DI_values stats: {dataframe['DI_values'].describe().to_string()}")
logger.info(f"DI discarded predictions: {len(dataframe[dataframe['do_predict'] == 0])}") logger.info(f"DI discarded predictions: {len(dataframe[dataframe['do_predict'] == 0])}")
# 清理数据
dataframe = dataframe.replace([np.inf, -np.inf], 0).ffill().fillna(0) dataframe = dataframe.replace([np.inf, -np.inf], 0).ffill().fillna(0)
logger.info(f"Final columns in populate_indicators: {list(dataframe.columns)}") logger.info(f"Final columns in populate_indicators: {list(dataframe.columns)}")
return dataframe return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
enter_long_conditions = [ enter_long_conditions = [
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"] + (5 if metadata["pair"] == "BTC/USDT" else 0)), qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]),
df["tema"] > df["tema"].shift(1), df["tema"] > df["tema"].shift(1),
df["volume"] > 0, df["volume"] > 0,
df["do_predict"] == 1, df["do_predict"] == 1
df["up_or_down"] == 1
] ]
if enter_long_conditions: df["entry_signal"] = reduce(lambda x, y: x & y, enter_long_conditions)
df.loc[ df["entry_signal"] = df["entry_signal"].rolling(window=2, min_periods=1).max().astype(bool)
reduce(lambda x, y: x & y, enter_long_conditions), df.loc[
["enter_long", "enter_tag"] df["entry_signal"],
] = (1, "long") ["enter_long", "enter_tag"]
] = (1, "long")
if df["entry_signal"].iloc[-1]:
logger.info(f"Entry signal triggered for {metadata['pair']}: rsi={df['rsi'].iloc[-1]}, buy_rsi_pred={df['buy_rsi_pred'].iloc[-1]}, do_predict={df['do_predict'].iloc[-1]}")
return df return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
exit_long_conditions = [ exit_long_conditions = [
(qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"])) | (qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"] - 5)) |
(df["close"] < df["close"].shift(1) * 0.98) | (df["close"] < df["close"].shift(1) * 0.98) |
(df["close"] < df["bb_lowerband"]), (df["close"] < df["bb_lowerband"]),
df["volume"] > 0, df["volume"] > 0,
df["do_predict"] == 1, df["do_predict"] == 1
df["up_or_down"] == 0
] ]
time_exit = (df["date"] >= df["date"].shift(1) + pd.Timedelta(days=1))
df.loc[ df.loc[
(reduce(lambda x, y: x & y, exit_long_conditions)) | time_exit, reduce(lambda x, y: x & y, exit_long_conditions),
"exit_long" "exit_long"
] = 1 ] = 1
return df return df
@ -300,9 +275,16 @@ class FreqaiExampleStrategy(IStrategy):
self, pair: str, order_type: str, amount: float, rate: float, self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time, entry_tag, side: str, **kwargs time_in_force: str, current_time, entry_tag, side: str, **kwargs
) -> bool: ) -> bool:
logger.info(f"Confirming trade entry for {pair}, order_type: {order_type}, rate: {rate}, current_time: {current_time}")
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = df.iloc[-1].squeeze() last_candle = df.iloc[-1].squeeze()
if side == "long": if side == "long":
if rate > (last_candle["close"] * (1 + 0.001)): if order_type == "market":
return False logger.info(f"Order confirmed for {pair}, rate: {rate} (market order)")
return True
if rate <= (last_candle["close"] * (1 + 0.01)):
logger.info(f"Order confirmed for {pair}, rate: {rate}")
return True
logger.info(f"Order rejected: rate {rate} exceeds threshold {last_candle['close'] * 1.01}")
return False
return True return True