first add

This commit is contained in:
zhangkun9038@dingtalk.com 2025-11-27 14:53:32 +08:00
parent f8cb87374f
commit 7e710dab1a
10 changed files with 810 additions and 475 deletions

View File

@ -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",

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 \

View File

@ -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 \

View File

@ -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

View 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 个蜡烛线报告一次(约 10h1h 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)

View File

@ -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