first add
This commit is contained in:
parent
f8cb87374f
commit
7e710dab1a
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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 \
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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
|
||||
|
||||
674
user_data/strategies/grid_manager.py
Normal file
674
user_data/strategies/grid_manager.py
Normal file
@ -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)
|
||||
@ -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
|
||||
def adjust_trade_position(self, trade, current_rate: float,
|
||||
current_profit: float, min_stake: float,
|
||||
max_stake: float, **kwargs) -> Optional[float]:
|
||||
"""
|
||||
加仓逻辑(暂未启用)
|
||||
"""
|
||||
return None
|
||||
Loading…
x
Reference in New Issue
Block a user