diff --git a/config_examples/staticgrid.json b/config_examples/staticgrid.json index 3144d43c..dd2ff98a 100644 --- a/config_examples/staticgrid.json +++ b/config_examples/staticgrid.json @@ -1,4 +1,7 @@ { + "redis": { + "url": "redis://192.168.1.154:6379/1" + }, "strategy": "StaticGrid", "max_open_trades": 150, "stake_currency": "USDT", @@ -8,9 +11,9 @@ "dry_run": false, "dry_run_wallet": 7766, "timeframe": "1h", - "position_adjustment_enable": false, + "position_adjustment_enable": true, "process_only_new_candles": false, - "max_entry_position_adjustment": 0, + "max_entry_position_adjustment": -1, "exchange": { "name": "okx", "key": "cbda9fde-b9e3-4a2d-94f9-e5c3705dfb5c", diff --git a/freqtrade/templates/mystrategy.json b/freqtrade/templates/mystrategy.json deleted file mode 100644 index e75652fa..00000000 --- a/freqtrade/templates/mystrategy.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "strategy_name": "MyStrategy", - "params": { - "roi": {}, - "stoploss": { - "stoploss": -0.14 - }, - "trailing": { - "trailing_stop": true, - "trailing_stop_positive": 0.0125, - "trailing_stop_positive_offset": 0.045, - "trailing_only_offset_is_reached": false - }, - "max_open_trades": { - "max_open_trades": 5 - }, - "buy": { - "bb_std": 3.0, - "bb_width_threshold": 0.012, - "h1_max_candles": 200, - "h1_max_consecutive_candles": 3, - "max_entry_adjustments": 4, - "rsi_bull_threshold": 54, - "rsi_length": 16, - "stochrsi_bull_threshold": 36, - "volume_multiplier": 1.6, - "add_position_callback": 0.053, - "bb_length": 14, - "bb_lower_deviation": 1.05, - "h1_rapid_rise_threshold": 0.065, - "min_condition_count": 2, - "rsi_oversold": 42, - "stake_divisor": 2.793, - "step_coefficient": 1.45, - "stochrsi_neutral_threshold": 29 - }, - "sell": { - "exit_bb_upper_deviation": 0.99, - "exit_volume_multiplier": 1.7, - "roi_param_a": -6e-05, - "roi_param_k": 132, - "roi_param_t": 0.168, - "rsi_overbought": 58 - }, - "protection": {} - }, - "ft_stratparam_v": 1, - "export_time": "2025-10-05 16:38:39.948030+00:00" -} diff --git a/freqtrade/templates/mystrategy.py b/freqtrade/templates/mystrategy.py deleted file mode 100644 index 36e53007..00000000 --- a/freqtrade/templates/mystrategy.py +++ /dev/null @@ -1,279 +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 - -logger = logging.getLogger(__name__) - -class MyStrategy(IStrategy): - minimal_roi = { - 0: 0.135, - 9: 0.052, - 15: 0.007, - 60: 0 - } - stoploss = -0.263 - trailing_stop = True - trailing_stop_positive = 0.324 - trailing_stop_positive_offset = 0.411 - trailing_only_offset_is_reached = False - max_open_trades = 4 - process_only_new_candles = True - use_exit_signal = True - startup_candle_count: int = 40 - can_short = False - - buy_rsi = IntParameter(low=10, high=50, default=30, space="buy", optimize=False, load=True) - sell_rsi = IntParameter(low=50, high=90, default=70, space="sell", optimize=False, load=True) - roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.135, space="roi", optimize=True, load=True) - roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.052, space="roi", optimize=True, load=True) - roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.007, space="roi", optimize=True, load=True) - stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.263, space="stoploss", optimize=True, load=True) - trailing_stop_positive_param = DecimalParameter(low=0.1, high=0.5, default=0.324, space="trailing", optimize=True, load=True) - trailing_stop_positive_offset_param = DecimalParameter(low=0.2, high=0.6, default=0.411, space="trailing", optimize=True, load=True) - - 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, - }, - "model_training_parameters": { - "n_estimators": 200, - "learning_rate": 0.05, - "num_leaves": 31, - "verbose": -1, - }, - } - - 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 feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: - dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) - dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) - dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period) - dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) - dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period) - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=period, stds=2.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"] - dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"] - dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) - dataframe["%-relative_volume-period"] = ( - dataframe["volume"] / dataframe["volume"].rolling(period).mean() - ) - dataframe = dataframe.replace([np.inf, -np.inf], 0) - dataframe = dataframe.ffill() - dataframe = dataframe.fillna(0) - return dataframe - - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: - dataframe["%-pct-change"] = dataframe["close"].pct_change() - dataframe["%-raw_volume"] = dataframe["volume"] - dataframe["%-raw_price"] = dataframe["close"] - dataframe = dataframe.replace([np.inf, -np.inf], 0) - dataframe = dataframe.ffill() - dataframe = dataframe.fillna(0) - return dataframe - - def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: - if len(dataframe["close"]) < 20: - logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整") - dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek - dataframe["%-hour_of_day"] = dataframe["date"].dt.hour - dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() - dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0) - dataframe["%-volatility"] = dataframe["%-volatility"].ffill() - dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0) - 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"] - if "%-volatility" not in dataframe.columns: - logger.warning("缺少 %-volatility 列,强制重新生成") - dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() - dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0) - dataframe["%-volatility"] = dataframe["%-volatility"].ffill() - dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0) - -# 移除 shift(-label_period),改为使用当前及过去的数据 - dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14) - dataframe["&-buy_rsi"] = dataframe["&-buy_rsi"].rolling(window=label_period).mean().ffill().bfill() - - for col in ["&-buy_rsi", "%-volatility"]: - 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(): - logger.warning(f"目标列 {col} 仍包含 NaN,数据预览:\n{dataframe[col].tail(10)}") - except Exception as e: - logger.error(f"创建 FreqAI 目标失败:{str(e)}") - raise - - logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}") - return dataframe - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - logger.info(f"处理交易对:{metadata['pair']}") - logger.debug(f"输入特征列:{list(dataframe.columns)}") - dataframe = self.freqai.start(dataframe, metadata, self) - logger.debug(f"FreqAI 输出特征列:{list(dataframe.columns)}") - - 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) - - label_period = self.freqai_info["feature_parameters"]["label_period_candles"] -# 使用滚动窗口而非未来函数来生成 up_or_down 列 - dataframe["up_or_down"] = np.where( - dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0 - ) - - if "&-buy_rsi" in dataframe.columns: - if "%-volatility" not in dataframe.columns: - logger.warning("缺少 %-volatility 列,强制重新生成") - dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() - dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0) - dataframe["%-volatility"] = dataframe["%-volatility"].ffill() - dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0) - - dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30 - dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05) - dataframe["&-roi_0"] = (dataframe["close"].rolling(window=label_period).mean() / dataframe["close"] - 1).clip(0, 0.2) - - for col in ["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0"]: - dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0) - dataframe[col] = dataframe[col].ffill() - dataframe[col] = dataframe[col].fillna(0) - - dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50) - dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90) - dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1) - dataframe["roi_0_pred"] = dataframe["&-roi_0"].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,填充为默认值") - dataframe[col] = dataframe[col].ffill() - dataframe[col] = dataframe[col].fillna(dataframe[col].mean()) - - dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3) - dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4) - - self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1]) - self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1]) - self.stoploss = float(self.stoploss_param.value) - 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 = float(self.trailing_stop_positive_param.value) - self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value) - - 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}") - else: - logger.warning(f"&-buy_rsi 列缺失,跳过 FreqAI 预测逻辑,检查 freqai.start 输出") - - dataframe = dataframe.replace([np.inf, -np.inf], 0) - dataframe = dataframe.ffill() - dataframe = dataframe.fillna(0) - - 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()}") - logger.debug(f"最终特征列:{list(dataframe.columns)}") - - return dataframe - - def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: - enter_long_conditions = [ - qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]), - df["tema"] > df["tema"].shift(1), - df["volume"] > 0, - df["do_predict"] == 1, - df["up_or_down"] == 1 - ] - if enter_long_conditions: - df.loc[ - reduce(lambda x, y: x & y, enter_long_conditions), - ["enter_long", "enter_tag"] - ] = (1, "long") - 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["close"].shift(1) * 0.97), - df["volume"] > 0, - df["do_predict"] == 1, - df["up_or_down"] == 0 - ] - if exit_long_conditions: - df.loc[ - reduce(lambda x, y: x & y, exit_long_conditions), - "exit_long" - ] = 1 - 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: - try: - df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - if df is None or df.empty: - logger.warning(f"无法获取 {pair} 的分析数据,拒绝交易") - return False - - last_candle = df.iloc[-1].squeeze() - if "close" not in last_candle or np.isnan(last_candle["close"]): - logger.warning(f"{pair} 的最新 K 线缺少有效 close 价格,拒绝交易") - return False - - if side == "long": - max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值 - if rate > max_rate: - logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}") - return False - elif side == "short": - logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})") - return False - - logger.debug(f"确认 {pair} 的交易:side={side}, rate={rate}, close={last_candle['close']}") - return True - except Exception as e: - logger.error(f"确认 {pair} 交易时出错:{str(e)}") - return False diff --git a/freqtrade/templates/smartbbgrid.py b/freqtrade/templates/smartbbgrid.py deleted file mode 100644 index 21b28282..00000000 --- a/freqtrade/templates/smartbbgrid.py +++ /dev/null @@ -1,19 +0,0 @@ -from freqtrade.strategy import IStrategy -from pandas import DataFrame -import talib.abstract as ta -import freqtrade.vendor.qtpylib.indicators as qtpylib -import numpy as np - -class SmartBBGrid(IStrategy): - INTERFACE_VERSION = 3 - timeframe = '4h' - can_short = False - minimal_roi = {"0": 100} - stoploss = -0.99 - startup_candle_count = 250 - use_exit_signal = True - exit_profit_only = False - ignore_roi_if_entry_signal = True - position_adjustment_enable = True - max_entry_position_adjustment = 1 - diff --git a/freqtrade/templates/staticgrid.py b/freqtrade/templates/staticgrid.py deleted file mode 100644 index f358e0db..00000000 --- a/freqtrade/templates/staticgrid.py +++ /dev/null @@ -1,100 +0,0 @@ -# /freqtrade/user_data/strategies/StaticGrid.py -from freqtrade.strategy import IStrategy, merge_informative_pair -from pandas import DataFrame -from typing import Optional, Dict, Any -import logging -import sys - -logger = logging.getLogger(__name__) - -class StaticGrid(IStrategy): - INTERFACE_VERSION = 3 - timeframe = '1h' - can_short = False - minimal_roi = {"0": 0.001} # 极小收益就卖 - stoploss = -0.99 - use_exit_signal = True - position_adjustment_enable = False # 关闭加仓 - - cooldown_candles = 0 - - # Grid parameters - LOWER = 1500.0 - UPPER = 4500.0 - STEP = 50.0 - STAKE = 40.0 - - def __init__(self, config: dict) -> None: - super().__init__(config) - print("[StaticGrid] Strategy initialized!", file=sys.stderr, flush=True) - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - return dataframe - - def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - 激进网格入场: - - 当价格低于任何网格点时,强制产生买入信号 - - 使用 enter_tag 来区分不同网格点 - """ - dataframe['enter_long'] = False - dataframe['enter_tag'] = '' - - if len(dataframe) == 0: - return dataframe - - current_price = dataframe['close'].iloc[-1] - current_low = dataframe['low'].iloc[-1] - print(f"[StaticGrid] Current price: {current_price:.2f}, low: {current_low:.2f}", file=sys.stderr, flush=True) - - # 生成所有网格点 - grid_levels = [self.LOWER + i * self.STEP for i in range(int((self.UPPER - self.LOWER) / self.STEP) + 1)] - - entry_count = 0 - - # 对每个网格点检查是否应该入场 - for grid_price in grid_levels: - # 激进条件:价格低于网格点就买 - if current_low <= grid_price: - dataframe.loc[dataframe.index[-1], 'enter_long'] = True - tag = f"grid_{int(grid_price)}" - # 累加所有适用的标签 - if dataframe.loc[dataframe.index[-1], 'enter_tag']: - dataframe.loc[dataframe.index[-1], 'enter_tag'] += f",{tag}" - else: - dataframe.loc[dataframe.index[-1], 'enter_tag'] = tag - entry_count += 1 - if entry_count <= 5: - print(f"[StaticGrid] Entry at grid {grid_price:.0f}", file=sys.stderr, flush=True) - - if entry_count > 5: - print(f"[StaticGrid] ... and {entry_count - 5} more grid levels", file=sys.stderr, flush=True) - print(f"[StaticGrid] Total entry signals: {entry_count}", file=sys.stderr, flush=True) - - return dataframe - - def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - 极端出场:任何上升就卖 - """ - dataframe['exit_long'] = False - - if len(dataframe) < 2: - return dataframe - - # 只要收盘价高于开盘价就卖出 - dataframe['exit_long'] = (dataframe['close'] > dataframe['open']) - - exits = dataframe['exit_long'].sum() - if exits > 0: - print(f"[StaticGrid] Exit signals: {exits}", file=sys.stderr, flush=True) - - return dataframe - - def adjust_trade_position(self, trade, current_rate: float, - current_profit: float, min_stake: float, - max_stake: float, **kwargs) -> Optional[float]: - """ - 不使用加仓 - """ - return None diff --git a/tools/backtest.sh b/tools/backtest.sh index da02e9b6..e3b64f71 100755 --- a/tools/backtest.sh +++ b/tools/backtest.sh @@ -313,7 +313,7 @@ echo "docker-compose run --rm freqtrade backtesting $PAIRS_FLAG \ --logfile /freqtrade/user_data/logs/freqtrade.log \ --config /freqtrade/config_examples/$CONFIG_FILE \ --strategy $STRATEGY_NAME \ - --strategy-path /freqtrade/templates \ + --strategy-path /freqtrade/user_data/strategies \ --timerange $START_DATE-$END_DATE \ --fee 0.0008 \ --breakdown day \ @@ -323,7 +323,7 @@ docker-compose run --rm freqtrade backtesting $PAIRS_FLAG \ --logfile /freqtrade/user_data/logs/freqtrade.log \ --config /freqtrade/config_examples/$CONFIG_FILE \ --strategy $STRATEGY_CLASS_NAME \ - --strategy-path /freqtrade/templates \ + --strategy-path /freqtrade/user_data/strategies \ --enable-protections \ --timerange $START_DATE-$END_DATE \ --fee 0.0008 \ diff --git a/tools/hyperopt_org.sh b/tools/hyperopt_org.sh index 6c1f5a23..c7490e41 100755 --- a/tools/hyperopt_org.sh +++ b/tools/hyperopt_org.sh @@ -477,9 +477,9 @@ echo "docker-compose run --rm freqtrade hyperopt $PAIRS_FLAG \ --freqaimodel LightGBMRegressorMultiTarget \ --strategy $STRATEGY_CLASS_NAME \ --config /freqtrade/config_examples/$CONFIG_FILE \ - --config /freqtrade/templates/${PARAMS_NAME}.json \ + --config /freqtrade/config_examples/${PARAMS_NAME}.json \ --enable-protections \ - --strategy-path /freqtrade/templates \ + --strategy-path /freqtrade/user_data/strategies \ --timerange ${START_DATE}-${END_DATE} \ --random-state $RANDOM_STATE \ --epochs $EPOCHS \ @@ -493,9 +493,9 @@ docker-compose run --rm freqtrade hyperopt $PAIRS_FLAG \ --freqaimodel LightGBMRegressorMultiTarget \ --strategy $STRATEGY_CLASS_NAME \ --config /freqtrade/config_examples/$CONFIG_FILE \ - --config /freqtrade/templates/${PARAMS_NAME}.json \ + --config /freqtrade/config_examples/${PARAMS_NAME}.json \ --enable-protections \ - --strategy-path /freqtrade/templates \ + --strategy-path /freqtrade/user_data/strategies \ --timerange ${START_DATE}-${END_DATE} \ --random-state $RANDOM_STATE \ --epochs $EPOCHS \ diff --git a/tools/live.sh b/tools/live.sh index 8774bc35..ce54da32 100755 --- a/tools/live.sh +++ b/tools/live.sh @@ -360,7 +360,7 @@ docker-compose run -d --rm \ --config /freqtrade/config_examples/$CONFIG_FILE \ --config /freqtrade/config_examples/live.json \ --strategy $STRATEGY_NAME \ - --strategy-path /freqtrade/templates + --strategy-path /freqtrade/user_data/strategies if [ $? -eq 0 ]; then echo "✅ 容器 $CONTAINER_NAME 启动完成" >&2 diff --git a/user_data/strategies/grid_manager.py b/user_data/strategies/grid_manager.py new file mode 100644 index 00000000..ac163bf1 --- /dev/null +++ b/user_data/strategies/grid_manager.py @@ -0,0 +1,674 @@ +""" +网格交易管理器 - 维护单个币对的完整网格持仓生命周期 +""" +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any +from enum import Enum +import sys +import json +from datetime import datetime +from urllib.parse import urlparse + +try: + import redis + REDIS_AVAILABLE = True +except ImportError: + REDIS_AVAILABLE = False + + +class AdjustmentType(Enum): + """加减仓类型""" + ENTRY = "entry" # 初始建仓 + ADD = "add_position" # 加仓 + REDUCE = "reduce_position" # 减仓 + EXIT = "exit" # 全部平仓 + + +@dataclass +class GridLevel: + """单个网格点记录""" + price: float # 网格价格 + quantity: float # 该价格的持仓数量 + entry_time: int # 建仓蜡烛线索引 + status: str # "open" 或 "closed" + + +@dataclass +class PositionRecord: + """单次加减仓记录""" + level_index: int # 网格级别序号 + price: float # 操作价格 + quantity: float # 操作份数 + type: AdjustmentType # 操作类型 + timestamp: int # 操作蜡烛线索引 + + +@dataclass +class OrderFill: + """订单成交信息""" + order_id: str # 订单 ID + pair: str # 币对 + side: str # "buy" 或 "sell" + filled_price: float # 成交价格 + filled_amount: float # 成交数量 + fee: float # 手续费(USDT) + timestamp: int # 成交蜡烛线索引 + reason: str = "" # 成交原因(entry/add/exit 等) + + +class GridManager: + """ + 网格交易管理器 + + 维护单个币对(如 ETH/USDT)的完整网格持仓生命周期 + 包含: + - 网格参数管理 + - 持仓状态跟踪 + - 加减仓决策 + """ + + def __init__(self, + pair: str, + lower_price: float, + upper_price: float, + step: float, + stake_per_grid: float): + """ + 初始化网格管理器 + + Args: + pair: 币对名称,如 "ETH/USDT" + lower_price: 网格下限价格,如 1500 + upper_price: 网格上限价格,如 4500 + step: 网格间距,如 50 + stake_per_grid: 每个网格的投资额,如 40 USDT + """ + self.pair = pair + self.lower_price = lower_price + self.upper_price = upper_price + self.step = step + self.stake_per_grid = stake_per_grid + + # 计算总网格数 + self.total_grid_levels = int((upper_price - lower_price) / step) + 1 + + # 生成所有网格点价格 + self.grid_prices = [lower_price + i * step for i in range(self.total_grid_levels)] + + # 持仓状态 + self.grid_levels: Dict[float, GridLevel] = {} # 价格 -> GridLevel + self.position_history: List[PositionRecord] = [] # 历史加减仓记录 + + # 订单执行历史 + self.order_fills: List[OrderFill] = [] # 所有订单成交记录 + self.pending_orders: Dict[str, PositionRecord] = {} # 待成交的订单映射 + + # 当前状态 + self.current_price: Optional[float] = None + self.total_quantity: float = 0.0 # 总持仓数量 + self.total_invested: float = 0.0 # 总投入金额 + self.avg_entry_price: float = 0.0 # 平均建仓价 + self.highest_price: float = 0.0 # 持仓期间最高价 + self.lowest_price: float = float('inf') # 持仓期间最低价 + self.max_positions: int = 0 # 历史最大持仓数 + + # 调试 + self.candle_index = 0 # 当前蜡烛线索引 + + # Redis 连接 + self.redis_client: Optional[redis.Redis] = None + self.redis_key = f"grid:{pair}" # Redis 存储键 + + # 报告 + self.last_report_candle = 0 # 上次报告的蜡烛线 + self.report_interval_candles = 600 # 每 600 个蜡烛线报告一次(约 10h,1h K线) + + def update_state(self, current_price: float, candle_index: int) -> None: + """ + 更新网格对象的当前状态 + + 这个方法应该在每个蜡烛线调用一次,用最新的市场价格更新网格对象 + + Args: + current_price: 当前市场价格 + candle_index: 当前蜡烛线索引(用于时间戳) + """ + self.current_price = current_price + self.candle_index = candle_index + + # 更新最高/最低价 + if self.total_quantity > 0: + if current_price > self.highest_price: + self.highest_price = current_price + if current_price < self.lowest_price: + self.lowest_price = current_price + + print(f"[GridManager] {self.pair} 状态更新 - 价格: {current_price:.2f}, " + f"持仓: {self.total_quantity:.6f}, 平均价: {self.avg_entry_price:.2f}", + file=sys.stderr, flush=True) + + def apply_adjustment(self, adjustment: PositionRecord) -> None: + """ + 应用一次加减仓操作(来自策略的决策) + + Args: + adjustment: PositionRecord 对象,包含加减仓的所有信息 + """ + price = adjustment.price + quantity = adjustment.quantity + adj_type = adjustment.type + + if adj_type == AdjustmentType.ENTRY or adj_type == AdjustmentType.ADD: + # 建仓或加仓 + if price not in self.grid_levels: + self.grid_levels[price] = GridLevel( + price=price, + quantity=quantity, + entry_time=self.candle_index, + status="open" + ) + else: + self.grid_levels[price].quantity += quantity + + # 更新总持仓 + old_total = self.total_quantity + self.total_invested += quantity * price + self.total_quantity += quantity + + # 更新平均价 + if self.total_quantity > 0: + self.avg_entry_price = self.total_invested / self.total_quantity + + # 更新最大持仓数 + if len(self.grid_levels) > self.max_positions: + self.max_positions = len(self.grid_levels) + + print(f"[GridManager] {self.pair} 加仓 - 价格: {price:.2f}, " + f"数量: {quantity:.6f}, 总持仓: {self.total_quantity:.6f}", + file=sys.stderr, flush=True) + + elif adj_type == AdjustmentType.REDUCE: + # 减仓 + if price in self.grid_levels: + self.grid_levels[price].quantity -= quantity + if self.grid_levels[price].quantity <= 0: + del self.grid_levels[price] + + self.total_quantity -= quantity + self.total_invested -= quantity * price + + if self.total_quantity > 0: + self.avg_entry_price = self.total_invested / self.total_quantity + + print(f"[GridManager] {self.pair} 减仓 - 价格: {price:.2f}, " + f"数量: {quantity:.6f}, 剩余持仓: {self.total_quantity:.6f}", + file=sys.stderr, flush=True) + + elif adj_type == AdjustmentType.EXIT: + # 全部平仓 + print(f"[GridManager] {self.pair} 全部平仓 - 持仓: {self.total_quantity:.6f}, " + f"平均价: {self.avg_entry_price:.2f}, 当前价: {price:.2f}", + file=sys.stderr, flush=True) + + self.grid_levels.clear() + self.total_quantity = 0.0 + self.total_invested = 0.0 + self.avg_entry_price = 0.0 + self.highest_price = 0.0 + self.lowest_price = float('inf') + + # 记录到历史 + self.position_history.append(adjustment) + + def decide_adjustment(self) -> Optional[PositionRecord]: + """ + 判定是否需要加减仓,并返回建议 + + 核心逻辑: + 1. 如果还没有建仓过,且价格在网格范围内 → 初始建仓 + 2. 如果已有头寸,价格跌入新的更低网格点 → 加仓 + 3. 如果已有头寸,价格涨超过平均价 → 全部平仓 + 4. 如果已有多个头寸,价格涨到最高点 → 可选:部分减仓 + + Returns: + PositionRecord 如果需要操作,否则 None + """ + if self.current_price is None: + return None + + # 情况 1: 还没有持仓,且价格在范围内 → 初始建仓 + if self.total_quantity == 0: + if self.lower_price <= self.current_price <= self.upper_price: + # 找到当前价格对应的网格点(向上舍入) + grid_price = (int(self.current_price / self.step) + 1) * self.step + if grid_price > self.upper_price: + grid_price = self.upper_price + + print(f"[GridManager] {self.pair} 初始建仓建议 - 价格: {grid_price:.2f}", + file=sys.stderr, flush=True) + + return PositionRecord( + level_index=self._price_to_level_index(grid_price), + price=self.current_price, + quantity=1.0, # 1个单位(实际金额由策略乘以 stake_per_grid) + type=AdjustmentType.ENTRY, + timestamp=self.candle_index + ) + + # 情况 2: 已有持仓,价格涨超过平均价 → 全部平仓 + if self.total_quantity > 0 and self.current_price > self.avg_entry_price: + profit_pct = (self.current_price - self.avg_entry_price) / self.avg_entry_price * 100 + print(f"[GridManager] {self.pair} 平仓建议 - 利润: {profit_pct:.2f}%", + file=sys.stderr, flush=True) + + return PositionRecord( + level_index=0, + price=self.current_price, + quantity=self.total_quantity, + type=AdjustmentType.EXIT, + timestamp=self.candle_index + ) + + # 情况 3: 已有持仓,价格跌入新的更低网格点 → 加仓 + if self.total_quantity > 0: + # 找到当前价格最接近的网格点(向下舍入) + current_grid_level = int(self.current_price / self.step) * self.step + + # 检查这个价格是否已经加仓过 + if current_grid_level not in self.grid_levels and current_grid_level >= self.lower_price: + # 检查是否还有加仓空间 + if len(self.grid_levels) < self.total_grid_levels: + print(f"[GridManager] {self.pair} 加仓建议 - 价格: {current_grid_level:.2f}, " + f"已有网格数: {len(self.grid_levels)}", + file=sys.stderr, flush=True) + + return PositionRecord( + level_index=self._price_to_level_index(current_grid_level), + price=self.current_price, + quantity=1.0, + type=AdjustmentType.ADD, + timestamp=self.candle_index + ) + + # 没有操作建议 + return None + + def _price_to_level_index(self, price: float) -> int: + """将价格转换为网格级别序号""" + index = int((price - self.lower_price) / self.step) + return max(0, min(index, self.total_grid_levels - 1)) + + def get_summary(self) -> Dict[str, Any]: + """获取当前持仓的完整摘要""" + return { + "pair": self.pair, + "current_price": self.current_price, + "total_quantity": self.total_quantity, + "total_invested": self.total_invested, + "avg_entry_price": self.avg_entry_price, + "unrealized_profit": (self.current_price - self.avg_entry_price) * self.total_quantity if self.total_quantity > 0 else 0, + "active_grid_levels": len(self.grid_levels), + "max_positions_ever": self.max_positions, + "total_adjustments": len(self.position_history), + "highest_price": self.highest_price if self.total_quantity > 0 else None, + "lowest_price": self.lowest_price if self.total_quantity > 0 else None, + } + + def record_order_fill(self, order_fill: OrderFill) -> None: + """ + 记录一个订单成交事件 + + 这个方法应该在订单成交时立即调用,将真实的成交数据反映到网格对象 + + Args: + order_fill: OrderFill 对象,包含成交的所有信息 + """ + self.order_fills.append(order_fill) + + if order_fill.side == "buy": + # 买入订单成交 + actual_cost = order_fill.filled_amount * order_fill.filled_price + order_fill.fee + + self.total_invested += actual_cost + self.total_quantity += order_fill.filled_amount + + # 更新平均价(考虑手续费) + if self.total_quantity > 0: + self.avg_entry_price = self.total_invested / self.total_quantity + + print(f"[GridManager] {self.pair} 买入订单成交 - " + f"价格: {order_fill.filled_price:.2f}, " + f"数量: {order_fill.filled_amount:.6f}, " + f"手续费: {order_fill.fee:.4f} USDT, " + f"总持仓: {self.total_quantity:.6f}", + file=sys.stderr, flush=True) + + # 记录到 grid_levels + if order_fill.filled_price not in self.grid_levels: + self.grid_levels[order_fill.filled_price] = GridLevel( + price=order_fill.filled_price, + quantity=order_fill.filled_amount, + entry_time=order_fill.timestamp, + status="open" + ) + else: + self.grid_levels[order_fill.filled_price].quantity += order_fill.filled_amount + + elif order_fill.side == "sell": + # 卖出订单成交 + sell_proceeds = order_fill.filled_amount * order_fill.filled_price - order_fill.fee + + self.total_invested -= order_fill.filled_amount * self.avg_entry_price + self.total_quantity -= order_fill.filled_amount + + # 更新平均价 + if self.total_quantity > 0: + self.avg_entry_price = self.total_invested / self.total_quantity + else: + self.avg_entry_price = 0.0 + + # 计算本次卖出的利润 + sell_profit = sell_proceeds - (order_fill.filled_amount * self.avg_entry_price if self.avg_entry_price > 0 else 0) + profit_pct = (order_fill.filled_price - self.avg_entry_price) / self.avg_entry_price * 100 if self.avg_entry_price > 0 else 0 + + print(f"[GridManager] {self.pair} 卖出订单成交 - " + f"价格: {order_fill.filled_price:.2f}, " + f"数量: {order_fill.filled_amount:.6f}, " + f"手续费: {order_fill.fee:.4f} USDT, " + f"利润: {sell_profit:.2f} USDT ({profit_pct:.2f}%), " + f"剩余持仓: {self.total_quantity:.6f}", + file=sys.stderr, flush=True) + + def sync_from_trade_object(self, trade: Any, candle_index: int) -> None: + """ + 从 Freqtrade 的 Trade 对象同步状态 + + 当无法直接捕获订单成交时,可以通过 Trade 对象反向同步状态 + (备选方案,不如直接记录 OrderFill 准确) + + Args: + trade: Freqtrade Trade 对象 + candle_index: 当前蜡烛线索引 + """ + # 从 trade 对象提取关键信息 + self.total_quantity = trade.amount + self.total_invested = trade.stake_amount + self.avg_entry_price = trade.open_rate + + # 如果 trade 有多个 entry(加仓的情况) + if hasattr(trade, 'trades') and trade.trades: + entries = [t for t in trade.trades if t.entry_side == 'entry'] + self.max_positions = len(entries) + + print(f"[GridManager] {self.pair} 从 Trade 对象同步状态 - " + f"持仓: {self.total_quantity:.6f}, " + f"投入: {self.total_invested:.2f} USDT, " + f"平均价: {self.avg_entry_price:.2f}", + file=sys.stderr, flush=True) + + def record_pending_order(self, order_id: str, adjustment: PositionRecord) -> None: + """ + 记录一个待成交的订单 + + 当策略决定建仓/加仓后,订单被提交给 Freqtrade + 这个方法记录订单 ID 与策略决策的映射关系 + + Args: + order_id: Freqtrade 返回的订单 ID + adjustment: 对应的 PositionRecord 对象 + """ + self.pending_orders[order_id] = adjustment + + print(f"[GridManager] {self.pair} 记录待成交订单 - " + f"订单ID: {order_id}, " + f"类型: {adjustment.type.value}, " + f"价格: {adjustment.price:.2f}", + file=sys.stderr, flush=True) + + def remove_pending_order(self, order_id: str) -> Optional[PositionRecord]: + """ + 移除待成交订单(当订单成交或取消时调用) + + Args: + order_id: 订单 ID + + Returns: + 对应的 PositionRecord,如果存在 + """ + return self.pending_orders.pop(order_id, None) + + def init_redis(self, redis_url: str) -> bool: + """ + 初始化 Redis 连接 + + Args: + redis_url: Redis URL,如 "redis://192.168.1.215:6379/0" + + Returns: + 是否连接成功 + """ + if not REDIS_AVAILABLE: + print(f"[GridManager] {self.pair} Redis 未安装,跳过 Redis 连接", + file=sys.stderr, flush=True) + return False + + try: + # 解析 Redis URL + parsed = urlparse(redis_url) + self.redis_client = redis.Redis( + host=parsed.hostname or 'localhost', + port=parsed.port or 6379, + db=int(parsed.path.strip('/') or 0), + decode_responses=True, + socket_connect_timeout=5, + socket_keepalive=True + ) + + # 测试连接 + self.redis_client.ping() + print(f"[GridManager] {self.pair} Redis 连接成功 - Key: {self.redis_key}", + file=sys.stderr, flush=True) + return True + + except Exception as e: + print(f"[GridManager] {self.pair} Redis 连接失败: {str(e)}", + file=sys.stderr, flush=True) + self.redis_client = None + return False + + def report_to_redis(self) -> None: + """ + 将当前状态同步到 Redis + """ + if not self.redis_client: + return + + try: + # 构成状态报告 + report = { + "timestamp": datetime.now().isoformat(), + "candle_index": self.candle_index, + "current_price": self.current_price, + "total_quantity": self.total_quantity, + "total_invested": self.total_invested, + "avg_entry_price": self.avg_entry_price, + "unrealized_profit": (self.current_price - self.avg_entry_price) * self.total_quantity if self.total_quantity > 0 else 0, + "active_grid_levels": len(self.grid_levels), + "max_positions_ever": self.max_positions, + "total_orders": len(self.order_fills), + "highest_price": self.highest_price if self.total_quantity > 0 else None, + "lowest_price": self.lowest_price if self.total_quantity > 0 else None, + } + + # 存储到 Redis + self.redis_client.set( + self.redis_key, + json.dumps(report), + ex=3600 # 1 小时过期 + ) + + except Exception as e: + print(f"[GridManager] {self.pair} Redis 写入失败: {str(e)}", + file=sys.stderr, flush=True) + + def generate_report(self) -> str: + """ + 生成人类可读的状态报告 + + Returns: + 格式化的状态报告字符串 + """ + unrealized = (self.current_price - self.avg_entry_price) * self.total_quantity if self.total_quantity > 0 else 0 + unrealized_pct = (self.current_price - self.avg_entry_price) / self.avg_entry_price * 100 if self.avg_entry_price > 0 else 0 + + report = f""" +{'='*80} +[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 网格交易报告 - {self.pair} +{'='*80} +【持仓状态】 + 当前价格: {self.current_price:.4f} USDT + 总持仓数量: {self.total_quantity:.8f} + 平均入场价: {self.avg_entry_price:.4f} USDT + 总成本投入: {self.total_invested:.2f} USDT + +【盈亏状态】 + 未实现盈亏: {unrealized:.2f} USDT + 未实现盈亏率: {unrealized_pct:.2f}% + 日期收益状态: {'亏损' if unrealized < 0 else '盈利'} + +【网格样态】 + 活跃网格点数: {len(self.grid_levels)} + 历史最大同时持仓: {self.max_positions} + 持仓期最高价: {self.highest_price:.4f if self.highest_price > 0 else 'N/A'} USDT + 持仓期最低价: {self.lowest_price:.4f if self.lowest_price != float('inf') else 'N/A'} USDT + 最低点亏损率: {(self.lowest_price - self.avg_entry_price) / self.avg_entry_price * 100 if self.lowest_price != float('inf') and self.avg_entry_price > 0 else 0:.2f}% + +【交易统计】 + 已成交订单数: {len(self.order_fills)} + 待成交订单数: {len(self.pending_orders)} + 历史加减仓次数: {len(self.position_history)} + 当前蜡烛线索引: {self.candle_index} +{'='*80} +""" + return report + + def check_and_report(self) -> None: + """ + 每隔指定间隔执行一次报告,并同步到 Redis + + 应该在 populate_indicators() 中调用 + """ + if self.candle_index - self.last_report_candle < self.report_interval_candles: + return + + # 打印报告 + report_text = self.generate_report() + print(report_text, file=sys.stderr, flush=True) + + # 同步到 Redis + self.report_to_redis() + + # 更新本次报告时间 + self.last_report_candle = self.candle_index + + print(f"[GridManager] {self.pair} 报告已同步到 Redis", + file=sys.stderr, flush=True) + + @classmethod + def recover_from_redis(cls, pair: str, redis_url: str) -> Optional[Dict[str, Any]]: + """ + 从 Redis 恢复一个 GridManager 对象 + + 当 Docker 容器重启后,通过 Redis 中的持仓状态恢复 GridManager + + Args: + pair: 币对名称,如 "BTC/USDT" + redis_url: Redis URL + + Returns: + 恢复后的 GridManager 对象,如果 Redis 中无数据返回 None + """ + if not REDIS_AVAILABLE: + print(f"[GridManager] {pair} Redis 未安装,无法恢复", + file=sys.stderr, flush=True) + return None + + try: + # 连接 Redis + parsed = urlparse(redis_url) + redis_client = redis.Redis( + host=parsed.hostname or 'localhost', + port=parsed.port or 6379, + db=int(parsed.path.strip('/') or 0), + decode_responses=True, + socket_connect_timeout=5 + ) + + # 测试连接 + redis_client.ping() + + # 尝试获取持仓数据 + redis_key = f"grid:{pair}" + data = redis_client.get(redis_key) + + if not data: + print(f"[GridManager] {pair} Redis 中无持仓数据,无法恢复", + file=sys.stderr, flush=True) + return None + + # 解析 JSON + state = json.loads(data) + + # 从 Redis 数据反向构建 GridManager + # 由于 Redis 中只有最新的快照,我们无法完全恢复所有历史 + # 但可以恢复当前的持仓状态 + print(f"[GridManager] {pair} 从 Redis 恢复持仓数据:", + file=sys.stderr, flush=True) + print(f" 持仓数量: {state.get('total_quantity')}", + file=sys.stderr, flush=True) + print(f" 平均价: {state.get('avg_entry_price')}", + file=sys.stderr, flush=True) + print(f" 总投入: {state.get('total_invested')}", + file=sys.stderr, flush=True) + + return state # 返回原始数据,让调用者处理恢复 + + except Exception as e: + print(f"[GridManager] {pair} 从 Redis 恢复失败: {str(e)}", + file=sys.stderr, flush=True) + return None + + def restore_from_redis_state(self, redis_state: Dict[str, Any]) -> None: + """ + 从 Redis 恢复的状态数据中还原对象内部状态 + + Args: + redis_state: 从 recover_from_redis() 获取的状态字典 + """ + try: + self.current_price = redis_state.get('current_price', 0) + self.total_quantity = redis_state.get('total_quantity', 0) + self.total_invested = redis_state.get('total_invested', 0) + self.avg_entry_price = redis_state.get('avg_entry_price', 0) + self.highest_price = redis_state.get('highest_price', 0) or 0 + self.lowest_price = redis_state.get('lowest_price', float('inf')) or float('inf') + self.max_positions = redis_state.get('max_positions_ever', 0) + self.candle_index = redis_state.get('candle_index', 0) + + # 重新初始化 grid_levels(从 avg_entry_price 推断) + if self.total_quantity > 0 and self.avg_entry_price > 0: + # 创建一个占位符网格点,表示当前持仓 + grid_price = round(self.avg_entry_price / self.step) * self.step + self.grid_levels[grid_price] = GridLevel( + price=grid_price, + quantity=self.total_quantity, + entry_time=self.candle_index, + status="open" + ) + + print(f"[GridManager] {self.pair} 状态已从 Redis 恢复", + file=sys.stderr, flush=True) + + except Exception as e: + print(f"[GridManager] {self.pair} 状态恢复失败: {str(e)}", + file=sys.stderr, flush=True) diff --git a/user_data/strategies/staticgrid.py b/user_data/strategies/staticgrid.py index 72754891..e2e43e30 100644 --- a/user_data/strategies/staticgrid.py +++ b/user_data/strategies/staticgrid.py @@ -1,43 +1,148 @@ # user_data/strategies/StaticGrid.py from freqtrade.strategy import IStrategy from pandas import DataFrame +from typing import Optional, Dict, Any +import logging +import sys +from grid_manager import GridManager, OrderFill, AdjustmentType + +logger = logging.getLogger(__name__) class StaticGrid(IStrategy): INTERFACE_VERSION = 3 timeframe = '1h' can_short = False - minimal_roi = {"0": 100} + minimal_roi = {"0": 0.001} # 极小收益就卖 stoploss = -0.99 use_exit_signal = True - position_adjustment_enable = True - max_entry_position_adjustment = -1 - - # 永续网格参数 + position_adjustment_enable = False # 关闭加仓 + + cooldown_candles = 0 + + # Grid parameters LOWER = 1500.0 UPPER = 4500.0 STEP = 50.0 STAKE = 40.0 + def __init__(self, config: dict) -> None: + super().__init__(config) + self.grid_managers: Dict[str, GridManager] = {} + self.redis_available = 'redis' in config and 'url' in config.get('redis', {}) + self.redis_url = config.get('redis', {}).get('url', '') + print("[StaticGrid] Strategy initialized!", file=sys.stderr, flush=True) + print(f"[StaticGrid] Redis available: {self.redis_available}", file=sys.stderr, flush=True) + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # 在每个蜡烛线更新网格管理器状态 + pair = metadata['pair'] + if pair not in self.grid_managers: + self._init_grid_manager(pair) + + grid_manager = self.grid_managers[pair] + current_price = dataframe['close'].iloc[-1] + candle_index = len(dataframe) - 1 + + # 更新网格管理器状态 + grid_manager.update_state(current_price, candle_index) + + # 定期报告 + grid_manager.check_and_report() + return dataframe + def _init_grid_manager(self, pair: str) -> None: + """初始化或恢复网格管理器""" + # 尝试从 Redis 恢复 + recovered_state = None + if self.redis_available: + recovered_state = GridManager.recover_from_redis(pair, self.redis_url) + + # 创建新的 GridManager + grid_manager = GridManager( + pair=pair, + lower_price=self.LOWER, + upper_price=self.UPPER, + step=self.STEP, + stake_per_grid=self.STAKE + ) + + # 如果 Redis 中有持仓数据,进行恢复 + if recovered_state: + grid_manager.restore_from_redis_state(recovered_state) + print(f"[StaticGrid] {pair} 已从 Redis 恢复", file=sys.stderr, flush=True) + else: + print(f"[StaticGrid] {pair} 新建持仓", file=sys.stderr, flush=True) + + # 初始化 Redis 连接用于后续同步 + if self.redis_available: + grid_manager.init_redis(self.redis_url) + + self.grid_managers[pair] = grid_manager + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - price = dataframe['close'].iloc[-1] - # 简化触发:只要当前价 <= 网格价,就买入(从当前价往下每 50 挂 20 张) - for i in range(1, 21): # 20 张买单 - grid_price = price - i * self.STEP - if grid_price >= self.LOWER: - dataframe.loc[dataframe['close'] <= grid_price, 'enter_long'] = True + """ + 网格入场逻辑 + """ + dataframe['enter_long'] = False + dataframe['enter_tag'] = '' + + if len(dataframe) == 0: + return dataframe + + pair = metadata['pair'] + if pair not in self.grid_managers: + self._init_grid_manager(pair) + + grid_manager = self.grid_managers[pair] + current_price = dataframe['close'].iloc[-1] + + # 询问网格管理器是否应该加仓 + adjustment = grid_manager.decide_adjustment() + + if adjustment: + if adjustment.type == AdjustmentType.ENTRY: + # 初始建仓 + dataframe.loc[dataframe.index[-1], 'enter_long'] = True + dataframe.loc[dataframe.index[-1], 'enter_tag'] = f"grid_entry_{current_price:.0f}" + print(f"[StaticGrid] {pair} 初始建仓信号 @ {current_price:.2f}", file=sys.stderr, flush=True) + elif adjustment.type == AdjustmentType.ADD: + # 加仓 + dataframe.loc[dataframe.index[-1], 'enter_long'] = True + dataframe.loc[dataframe.index[-1], 'enter_tag'] = f"grid_add_{current_price:.0f}" + print(f"[StaticGrid] {pair} 加仓信号 @ {current_price:.2f}", file=sys.stderr, flush=True) + return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - price = dataframe['close'].iloc[-1] - # 简化触发:只要当前价 >= 网格价,就卖出(从当前价往上每 50 挂 10 张) - for i in range(1, 11): # 10 张卖单 - grid_price = price + i * self.STEP - if grid_price <= self.UPPER: - dataframe.loc[dataframe['close'] >= grid_price, 'exit_long'] = True + """ + 网格出场逻辑 + """ + dataframe['exit_long'] = False + + if len(dataframe) == 0: + return dataframe + + pair = metadata['pair'] + if pair not in self.grid_managers: + return dataframe + + grid_manager = self.grid_managers[pair] + + # 询问网格管理器是否应该平仓 + adjustment = grid_manager.decide_adjustment() + + if adjustment and adjustment.type == AdjustmentType.EXIT: + # 平仓 + dataframe.loc[dataframe.index[-1], 'exit_long'] = True + print(f"[StaticGrid] {pair} 平仓信号 @ {grid_manager.current_price:.2f}", file=sys.stderr, flush=True) + return dataframe - def custom_stake_amount(self, **kwargs) -> float: - return self.STAKE \ No newline at end of file + def adjust_trade_position(self, trade, current_rate: float, + current_profit: float, min_stake: float, + max_stake: float, **kwargs) -> Optional[float]: + """ + 加仓逻辑(暂未启用) + """ + return None \ No newline at end of file